11 Commits

Author SHA1 Message Date
Aarni Halinen e83b4d4624 Add translations to password reset 2017-10-31 22:34:27 +02:00
Aarni Halinen 38225cabc8 Write password recovery emails in html 2017-10-31 22:34:27 +02:00
henu 08bb63ce1f Add margin to footer 2017-10-31 22:27:51 +02:00
henu 72a93e1dfd Merge branch 'develop' of sika.sahkoinsinoorikilta.fi:vtmk/web2.0 into feature-refactor-webapp-styles 2017-10-31 22:24:37 +02:00
henu fa5597f7cf Add centering back to navigation 2017-10-31 22:24:15 +02:00
henu 92ea427c53 Add copyright icon to footer 2017-10-31 22:23:36 +02:00
henu abc2519bc7 Add collapse property for navbar 2017-10-31 22:18:30 +02:00
henu 87a0c68ef2 Update meta tag 2017-10-31 22:17:44 +02:00
henu 612b17960a Make fa-icons larger and add padding between them 2017-10-31 22:16:52 +02:00
Aarni Halinen 7f21b7bba2 Add bootstrap4 to password recovery html 2017-10-31 21:37:19 +02:00
henu 8c116d58de Remove unnecessary comments 2017-10-31 21:17:36 +02:00
443 changed files with 7069 additions and 18974 deletions
-4
View File
@@ -1,7 +1,3 @@
[report]
show_missing = True
[run] [run]
omit = omit =
*/migrations/* */migrations/*
*/admin.py
*/translation.py
-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 -12
View File
@@ -1,15 +1,20 @@
.DS_Store *.swp
.env sikweb/settings.py
*~
*.pyc *.pyc
/collected_static/ *.sqlite3
/media/ uwsgi.ini
logs/ uwsgi.log
infoscreen/static/js/hsl.json
members/logs/* members/logs/*
node_modules/
.coverage
.vscode/
.idea/ .idea/
*.code-workspace logs/
venv/ /media/
.venv/ node_modules/
poetry.lock /.coverage
db.sqlite3
requirements_henu.txt
/collected_static/
mydatabase
settings.json
.vscode/
+90 -181
View File
@@ -1,191 +1,100 @@
stages: stages:
- setup - test
- audit - lint
- lint - publish
- test - deploy
- 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 pip==25.3
- pip install poetry==2.1.1
- poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi
script:
- safety check
test: test:
image: python:3.12.9 image: python:3.5
stage: test stage: test
only: services:
- pushes - postgres:latest
needs: [] variables:
services: POSTGRES_DB: ci
- postgres:12 POSTGRES_USER: postgres
variables: POSTGRES_PASSWORD: postgres
POSTGRES_DB: ci DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
POSTGRES_USER: postgres script:
POSTGRES_PASSWORD: postgres - python -V
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" - pip install -r requirements.txt
DB_HOST: postgres - cp sikweb/settings-sample.py sikweb/default_settings.py
before_script: - cp sikweb/.ci-settings.py sikweb/settings.py
- pip install pip==25.3 - python manage.py migrate --noinput
- pip install poetry==2.1.1 - python manage.py createdefaultadmin
- poetry config virtualenvs.create false - python manage.py test
- poetry install --no-interaction --no-ansi
script:
- python manage.py migrate --noinput
- python manage.py createdefaultadmin
- python manage.py test
lint:py: pycodestyle:
image: python:3.12.9 image: python:3.5
stage: lint stage: lint
only: script:
- pushes - pip install pycodestyle
needs: [] - pycodestyle --config=setup.cfg --count .
script:
- pip install black==22.3.0
- black --check .
lint:js: eslint:
image: node:22 image: node:7.10.0
stage: lint stage: lint
only: before_script:
- pushes - npm install
needs: ["install"] script:
script: - npm run eslint
- npm run lint:js
lint:md: remark:
image: node:22 image: node:7.10.0
stage: lint stage: lint
only: before_script:
- pushes - npm install
needs: ["install"] script:
script: - npm run remark
- npm run lint:md
publish: publish:
image: docker:25-cli stage: publish
stage: publish image: docker:latest
needs: ["test", "lint:py", "lint:js", "lint:md"] only:
services: - develop
- docker:25-dind before_script:
only: - docker info
- main - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY_URL
- production script:
script: - docker build . -t "$IMAGE_NAME"
- docker info - docker push "$IMAGE_NAME"
- 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:
image: docker:25-cli stage: deploy
stage: deploy image: alpine:latest
only: environment:
- main name: dev
environment: url: http://web.sik.party:8080
name: dev only:
url: http://api.dev.sahkoinsinoorikilta.fi - develop
variables: before_script:
DOCKER_HOST: $DEV_CI_DOCKER_HOST - pwd
DOCKER_TLS_VERIFY: 1 - apk add --update openssh
before_script: - ssh -V
- mkdir -p ~/.docker - mkdir -p ~/.ssh
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem - chmod 600 ~/.ssh/id_rsa
- echo "$DEV_TLSKEY" > ~/.docker/key.pem - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY script:
script: - scp docker-compose.yml $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/docker-compose.yml
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME" - ssh $DEV_SSH_USER@$DEV_SSH_HOST "cd deployment && docker-compose down && docker pull \"$IMAGE_NAME\" && docker-compose up -d && docker image prune -f"
after_script:
- docker logout "$CI_REGISTRY"
deploy:production: deploy_production:
stage: deploy stage: deploy
image: docker:25-cli image: alpine:latest
only: environment:
- production name: production
environment: url: https://sika.sahkoinsinoorikilta.fi
name: production when: manual
url: https://api.sahkoinsinoorikilta.fi only:
when: manual - master
variables: before_script:
DOCKER_HOST: $CI_DOCKER_HOST - pwd
DOCKER_TLS_VERIFY: 1 - apk add --update openssh
before_script: - ssh -V
- mkdir -p ~/.docker - mkdir -p ~/.ssh
- echo "$TLSCACERT" > ~/.docker/ca.pem - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- echo "$TLSCERT" > ~/.docker/cert.pem - chmod 600 ~/.ssh/id_rsa
- echo "$TLSKEY" > ~/.docker/key.pem - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY script:
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"
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"
-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
-2
View File
@@ -1,2 +0,0 @@
python 3.12.9
poetry 2.1.1
+7 -28
View File
@@ -1,29 +1,8 @@
FROM python:3.12.9-slim-bullseye AS builder FROM python:3
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
COPY . ./ ENV IS_DOCKER 1
ENV POETRY_VERSION=2.1.1 RUN mkdir /code
RUN pip install pip==25.3 WORKDIR /code
RUN pip install "poetry==$POETRY_VERSION" ADD requirements.txt /code/
RUN poetry self add poetry-plugin-export RUN env
RUN poetry export --without-hashes --format=requirements.txt --output requirements.txt ADD . /code/
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 pip==25.3
RUN pip install --no-deps -r requirements.txt
RUN python manage.py collectstatic --noinput
CMD ["sh", "-c", "./production_entrypoint.sh"]
-175
View File
@@ -1,175 +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
```
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.1.1
```
Install dependencies with
```bash
poetry install
```
Poetry is configured to install dependencies in a virtual environment, so you should see `.venv` folder in repo root.
Activate virtual environment in shell
```bash
eval $(poetry env activate)
```
### 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
```
See [Linting](#linting) for more info
### 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
Install dependencies with
```bash
poetry install
```
and make sure you are using Python from your virutal environment.
Virtual environment can be activated with
```bash
eval $(poetry env activate)
```
and you verify correct Python executable with
```bash
which python
# should return path similar to {your-system path}/web2.0-backend/.venv/bin/python
```
### 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 `main` 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 `main`, but if the fix needs a notable amount of work, it should be done in a `bugfix` branch instead.
Merge requests to `main` should be reviewed by multiple developers. Only a moderator can accept merge requests to `production`.
### 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 `main` or `production`.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+23
View File
@@ -0,0 +1,23 @@
from django.apps import AppConfig
import logging
import sys
from coffee_scale import mqtt
class CoffeeScaleConfig(AppConfig):
name = 'coffee_scale'
def ready(self):
if ('makemigrations' in sys.argv or 'migrate' in sys.argv):
return
try:
logging.info('Connecting to MQTT (coffee scale) at {}...'.format(mqtt.HOST))
logging.info('If there is no confirmation, the MQTT connection has probably failed.')
mqtt.client.connect_async(mqtt.HOST, mqtt.PORT, 60)
mqtt.client.loop_start()
except Exception as ex:
logging.error(ex)
logging.error('Failed to connect to MQTT at {}'.format(mqtt.HOST))
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+58
View File
@@ -0,0 +1,58 @@
import paho.mqtt.client as mqtt
import logging
import datetime
from collections import deque
from django.conf import settings
HOST = settings.MQTT_SETTINGS['HOST']
PORT = settings.MQTT_SETTINGS['PORT']
TOPICS = settings.MQTT_SETTINGS['TOPICS']
latest = {}
def on_connect(client, userdata, flags, rc):
logging.info('Connected successfully to MQTT.')
logging.info('Subscribing to all topics on {}.'.format(HOST))
client.subscribe('sik/kiltahuone/kahvivaaka/#')
def update_latest(msg):
payload = msg.payload.decode('utf-8')
if msg.topic == TOPICS['WEIGHT']:
weight = float(payload)
latest['weight'] = weight
elif msg.topic == TOPICS['CUPS']:
cups = float(payload)
latest['cups'] = cups
elif msg.topic == TOPICS['BREWING']:
brewing = bool(int(payload))
latest['brewing'] = brewing
elif msg.topic == TOPICS['BREW_TIME']:
brew_time = datetime.datetime.fromtimestamp(float(payload))
latest['brew_time'] = brew_time
def on_message(client, userdata, msg):
try:
update_latest(msg)
except Exception as ex:
logging.exception('Failed to parse MQTT payload.')
def on_disconnect(client, userdata, rc):
if rc != 0:
logging.warning('MQTT unexpectedly disconnected.')
else:
client.loop_stop(force=False)
logging.warning('MQTT disconnected.')
def get_latest():
return latest
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
+121
View File
@@ -0,0 +1,121 @@
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);}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

+130
View File
@@ -0,0 +1,130 @@
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 = "&infin;";
$("#text").addClass("unknown");
}
else if(brewing){
cups = "+";
brewtext = ":)";
$("#text").addClass("brewing");
}
else if(cups <= 2){
$("#text").addClass("hurry");
}
formatBrewTime();
if($("#text").html() == "+" && !brewing)
$("body").addClass("coffeeready");
var cupsString = cups.toString();
len = cupsString.length;
$("#text").html(cups);
}
function updateTime(){
var now = new Date();
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
}
function formatTime(hours, minutes, seconds){
var str = "";
if(hours < 10)
str += "0";
str += hours;
str += ":";
if(minutes < 10)
str += "0";
str += minutes;
str += ":";
if(seconds < 10)
str += "0";
str += seconds;
return str;
}
function resize(){
var w = $("#container").width();
var h = $("#container").height();
var s = w > h ? h : w;
var font = s*0.8*0.38/Math.sqrt(len);
$("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'});
}
+34
View File
@@ -0,0 +1,34 @@
<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>
+30
View File
@@ -0,0 +1,30 @@
from django.test import TestCase, Client
from django.conf import settings
from coffee_scale.mqtt import on_message
HOST = settings.MQTT_SETTINGS['HOST']
PORT = settings.MQTT_SETTINGS['PORT']
TOPICS = settings.MQTT_SETTINGS['TOPICS']
class MQTTTestCase(TestCase):
"""Tests MQTT functionality"""
class MockMessage:
def __init__(self, payload, topic):
self.payload = payload
self.topic = topic
def setUp(self):
payload = '10'.encode('utf-8')
topic = TOPICS['CUPS']
msg = MQTTTestCase.MockMessage(payload, topic)
on_message(None, None, msg)
self.c = Client()
def test_receive_cups(self):
response = self.c.get('/coffee/cups')
payload = response.json()
self.assertEquals(payload['cups'], 10)
+14
View File
@@ -0,0 +1,14 @@
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),
]
+27
View File
@@ -0,0 +1,27 @@
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)
+6 -25
View File
@@ -1,32 +1,13 @@
version: '3'
services: services:
db: db:
image: postgres:12 image: postgres
volumes:
- dbdata:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=postgres
web: web:
build: . build: .
environment: image: 86.50.143.82:5000/web20
- DEPLOY_ENV=local 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"]
- 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='{}'
ports: ports:
- 8000:8000 - "8080:8080"
depends_on: depends_on:
- db - 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
}
];
+4 -10
View File
@@ -1,16 +1,10 @@
"""Admin site registers.""" """Admin site registers."""
from django.contrib import admin from django.contrib import admin
from infoscreen.models import ( from infoscreen.models import Rotation, InfoItem, InfoInstance
Rotation, from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem
InfoItem, from infoscreen.models import ExternalWebsiteInfoItem
InfoInstance, from infoscreen.models import VideoInfoItem
ImageInfoItem,
ExternalImageInfoItem,
ABBInfoItem,
ExternalWebsiteInfoItem,
VideoInfoItem,
)
# Register your models here. # Register your models here.
admin.site.register(Rotation) admin.site.register(Rotation)
+1 -1
View File
@@ -6,4 +6,4 @@ from django.apps import AppConfig
class InfoscreenConfig(AppConfig): class InfoscreenConfig(AppConfig):
"""Infoscreen app configuration.""" """Infoscreen app configuration."""
name = "infoscreen" name = 'infoscreen'
+69
View File
@@ -0,0 +1,69 @@
"""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
+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 initial = True
dependencies = [ dependencies = [
("contenttypes", "0002_remove_content_type_name"), ('contenttypes', '0002_remove_content_type_name'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="HSLDataModel", name='HSLDataModel',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('data', models.TextField(default='', editable=False)),
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("data", models.TextField(default="", editable=False)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="InfoInstance", name='InfoInstance',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('duration', models.FloatField(default=15.0)),
models.AutoField( ('item_id', models.PositiveIntegerField()),
auto_created=True, ('item_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
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( migrations.CreateModel(
name="InfoItem", name='InfoItem',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255)),
models.AutoField( ('expire_date', models.DateTimeField(blank=True, null=True)),
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( migrations.CreateModel(
name="Rotation", name='Rotation',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255)),
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name="ABBInfoItem", name='ABBInfoItem',
fields=[ 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( migrations.CreateModel(
name="ExternalImageInfoItem", name='ExternalImageInfoItem',
fields=[ 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", ('url', models.TextField()),
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( migrations.CreateModel(
name="HslInfoItem", name='HslInfoItem',
fields=[ 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( migrations.CreateModel(
name="ImageInfoItem", name='ImageInfoItem',
fields=[ 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", ('img', models.ImageField(upload_to='infoimages/')),
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( migrations.CreateModel(
name="SossoInfoItem", name='SossoInfoItem',
fields=[ 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( migrations.AddField(
model_name="infoinstance", model_name='infoinstance',
name="rotation", name='rotation',
field=models.ForeignKey( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='infoscreen.Rotation'),
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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("infoscreen", "0001_initial"), ('infoscreen', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="CoffeeInfoItem", name='CoffeeInfoItem',
fields=[ 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("infoscreen", "0002_coffeeinfoitem"), ('infoscreen', '0002_coffeeinfoitem'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="ApyInfoItem", name='ApyInfoItem',
fields=[ 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( migrations.CreateModel(
name="EventInfoItem", name='EventInfoItem',
fields=[ 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( migrations.CreateModel(
name="ExternalWebsiteInfoItem", name='ExternalWebsiteInfoItem',
fields=[ 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", ('url', models.TextField()),
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( migrations.DeleteModel(
name="CoffeeInfoItem", name='CoffeeInfoItem',
), ),
] ]
+5 -15
View File
@@ -9,26 +9,16 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("infoscreen", "0003_auto_20170329_1857"), ('infoscreen', '0003_auto_20170329_1857'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="VideoInfoItem", name='VideoInfoItem',
fields=[ 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", ('video', models.FileField(upload_to='infovideos/')),
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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("infoscreen", "0004_videoinfoitem"), ('infoscreen', '0004_videoinfoitem'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name="externalimageinfoitem", model_name='externalimageinfoitem',
name="url", name='url',
field=models.URLField(), field=models.URLField(),
), ),
migrations.AlterField( migrations.AlterField(
model_name="externalwebsiteinfoitem", model_name='externalwebsiteinfoitem',
name="url", name='url',
field=models.URLField(), field=models.URLField(),
), ),
] ]
@@ -8,14 +8,11 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("infoscreen", "0005_auto_20170913_1841"), ('infoscreen', '0005_auto_20170913_1841'),
] ]
operations = [ operations = [
migrations.DeleteModel( migrations.DeleteModel(
name="HSLDataModel", name='HSLDataModel',
),
migrations.DeleteModel(
name="HslInfoItem",
), ),
] ]
-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),
),
]
+85 -81
View File
@@ -7,7 +7,7 @@ from django import forms
from django.utils import timezone from django.utils import timezone
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType 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): class InfoItem(models.Model):
@@ -16,7 +16,6 @@ class InfoItem(models.Model):
class __meta__: class __meta__:
abstract = True abstract = True
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
# expire_date = None means never expiring item # expire_date = None means never expiring item
expire_date = models.DateTimeField(blank=True, null=True) expire_date = models.DateTimeField(blank=True, null=True)
@@ -24,14 +23,14 @@ class InfoItem(models.Model):
def get_template_url(self): def get_template_url(self):
"""Get infoscreen template url.""" """Get infoscreen template url."""
raise NotImplementedError("inheriting classes must implement get_template_url") raise NotImplementedError(
"inheriting classes must implement get_template_url")
@staticmethod @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Get create infoscreen template url command.""" """Get create infoscreen template url command."""
raise NotImplementedError( raise NotImplementedError(
"inheriting classes must implement get_create_template_url" "inheriting classes must implement get_create_template_url")
)
@classmethod @classmethod
def create_from_dict(cls, d): def create_from_dict(cls, d):
@@ -43,13 +42,14 @@ class InfoItem(models.Model):
def update_from_dict(self, d): def update_from_dict(self, d):
"""Update model based on given dict.""" """Update model based on given dict."""
try: try:
expire_date = d.pop("expire_date", None) expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except: except:
pass pass
dmap = { dmap = {
"name": "name", 'name': 'name',
} }
for k, v in d.items(): for k, v in d.items():
try: try:
@@ -61,13 +61,13 @@ class InfoItem(models.Model):
def get_dict(self): def get_dict(self):
"""Convert django model to dict and return it.""" """Convert django model to dict and return it."""
return { return {
"id": self.id, 'id': self.id,
"name": self.name, 'name': self.name,
"item_type": ContentType.objects.get_for_model(self).id, 'item_type': ContentType.objects.get_for_model(self).id,
"template_url": self.get_template_url(), 'template_url': self.get_template_url(),
"display_name": self.display_name, 'display_name': self.display_name,
"create_template_url": self.get_create_template_url(), 'create_template_url': self.get_create_template_url(),
"options": {}, 'options': {}
} }
def delete(self): def delete(self):
@@ -75,8 +75,8 @@ class InfoItem(models.Model):
# since generic foreign keys suck, delete info # since generic foreign keys suck, delete info
# items pointing here manually # items pointing here manually
InfoInstance.objects.filter( InfoInstance.objects.filter(
item_id=self.id, item_type=ContentType.objects.get_for_model(self) item_id=self.id,
).delete() item_type=ContentType.objects.get_for_model(self)).delete()
super().delete() super().delete()
@classmethod @classmethod
@@ -98,12 +98,12 @@ class ABBInfoItem(InfoItem):
def get_template_url(self): def get_template_url(self):
"""Return ABB infoitem template url.""" """Return ABB infoitem template url."""
return "/static/infoscreen/html/abb.html" return "/static/html/abb.html"
@staticmethod @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create ABB infoitem template url command.""" """Call create ABB infoitem template url command."""
return "/static/infoscreen/html/abb_create.html" return "/static/html/abb_create.html"
class ApyInfoItem(InfoItem): class ApyInfoItem(InfoItem):
@@ -113,12 +113,12 @@ class ApyInfoItem(InfoItem):
def get_template_url(self): def get_template_url(self):
"""Return APY infoitem template url.""" """Return APY infoitem template url."""
return "/static/infoscreen/html/apy.html" return "/static/html/apy.html"
@staticmethod @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create APY infoitem template url command.""" """Call create APY infoitem template url command."""
return "/static/infoscreen/html/apy_create.html" return "/static/html/apy_create.html"
class ExternalWebsiteInfoItem(InfoItem): class ExternalWebsiteInfoItem(InfoItem):
@@ -129,17 +129,17 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_template_url(self): def get_template_url(self):
"""Return external website infoitem template url.""" """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 @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create external website infoitem template url command.""" """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): def get_dict(self):
"""Convert django model to dict and return it.""" """Convert django model to dict and return it."""
d = super().get_dict() d = super().get_dict()
d["options"] = {"url": self.url} d["options"] = {'url': self.url}
return d return d
@classmethod @classmethod
@@ -152,22 +152,23 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_list(self): def get_list(self):
"""Return list containing infoitem data.""" """Return list containing infoitem data."""
return { return {
"id": self.id, 'id': self.id,
"name": self.name, 'name': self.name,
"url": self.url, 'url': self.url,
} }
def update_from_dict(self, d): def update_from_dict(self, d):
"""Update model based on given dict.""" """Update model based on given dict."""
try: try:
expire_date = d.pop("expire_date", None) expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except: except:
pass pass
dmap = { dmap = {
"name": "name", 'name': 'name',
"url": "url", 'url': 'url',
} }
for k, v in d.items(): for k, v in d.items():
try: try:
@@ -184,25 +185,12 @@ class SossoInfoItem(InfoItem):
def get_template_url(self): def get_template_url(self):
"""Return Sosso infoitem template url.""" """Return Sosso infoitem template url."""
return "/static/infoscreen/html/sosso.html" return "/static/html/sosso.html"
@staticmethod @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create Sosso infoitem template url command.""" """Call create Sosso infoitem template url command."""
return "/static/infoscreen/html/sosso_create.html" return "/static/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): class EventInfoItem(InfoItem):
@@ -212,12 +200,12 @@ class EventInfoItem(InfoItem):
def get_template_url(self): def get_template_url(self):
"""Return Event infoitem template url.""" """Return Event infoitem template url."""
return "/static/infoscreen/html/events.html" return "/static/html/events.html"
@staticmethod @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create Event infoitem template url command.""" """Call create Event infoitem template url command."""
return "/static/infoscreen/html/events_create.html" return "/static/html/events_create.html"
class ImageInfoItem(InfoItem): class ImageInfoItem(InfoItem):
@@ -230,42 +218,57 @@ class ImageInfoItem(InfoItem):
"""Return Image infoitem template url.""" """Return Image infoitem template url."""
# get param to avoid angular from optimizing same template # get param to avoid angular from optimizing same template
# with different options # 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 @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create Image infoitem template url command.""" """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): def get_dict(self):
"""Convert django model to dict and return it.""" """Convert django model to dict and return it."""
d = super().get_dict() d = super().get_dict()
d["options"] = {"img": self.img.url} d["options"] = {'img': self.img.url}
return d return d
class VideoInfoItem(InfoItem): class VideoInfoItem(InfoItem):
"""Class for Video Infoscreen item.""" """Class for Video Infoscreen item."""
display_name = "Video" display_name = ("Video")
video = models.FileField(upload_to="infovideos/") video = models.FileField(upload_to="infovideos/")
def get_template_url(self): def get_template_url(self):
"""Return Video infoitem template url.""" """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 @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create Video infoitem template url command.""" """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): def get_dict(self):
"""Convert django model to dict and return it.""" """Convert django model to dict and return it."""
d = super().get_dict() d = super().get_dict()
d["options"] = {"video": self.video.url} d["options"] = {'video': self.video.url}
return d 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 ExternalImageInfoItem(InfoItem):
"""Class for External Image Infoscreen item.""" """Class for External Image Infoscreen item."""
@@ -274,17 +277,17 @@ class ExternalImageInfoItem(InfoItem):
def get_template_url(self): def get_template_url(self):
"""Return External Image infoitem template url.""" """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 @staticmethod
def get_create_template_url(): def get_create_template_url():
"""Call create External Image infoitem template url command.""" """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): def get_dict(self):
"""Convert django model to dict and return it.""" """Convert django model to dict and return it."""
d = super().get_dict() d = super().get_dict()
d["options"] = {"img": self.url} d["options"] = {'img': self.url}
return d return d
@classmethod @classmethod
@@ -297,14 +300,15 @@ class ExternalImageInfoItem(InfoItem):
def update_from_dict(self, d): def update_from_dict(self, d):
"""Update model based on given dict.""" """Update model based on given dict."""
try: try:
expire_date = d.pop("expire_date", None) expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except: except:
pass pass
dmap = { dmap = {
"name": "name", 'name': 'name',
"url": "url", 'url': 'url',
} }
for k, v in d.items(): for k, v in d.items():
try: try:
@@ -317,15 +321,12 @@ class ExternalImageInfoItem(InfoItem):
class InfoInstance(models.Model): class InfoInstance(models.Model):
"""Class for Info instance in Infoscreen.""" """Class for Info instance in Infoscreen."""
id = models.AutoField(primary_key=True) 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 duration = models.FloatField(default=15.0) # seconds
# generic relation to some kind of InfoItem # generic relation to some kind of InfoItem
item_id = models.PositiveIntegerField() item_id = models.PositiveIntegerField()
item_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) item_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
item = GenericForeignKey("item_type", "item_id") item = GenericForeignKey('item_type', 'item_id')
@classmethod @classmethod
def create_from_dict(cls, d): def create_from_dict(cls, d):
@@ -338,27 +339,31 @@ class InfoInstance(models.Model):
except: except:
raise RuntimeError("invalid parameters supplied supplied") raise RuntimeError("invalid parameters supplied supplied")
try: try:
return cls.objects.create(rotation=rotation, item=item, duration=duration) return cls.objects.create(
rotation=rotation,
item=item,
duration=duration
)
except: except:
raise RuntimeError("error while adding instance to db") raise RuntimeError("error while adding instance to db")
def get_dict(self): def get_dict(self):
"""Convert django model to dict and return it.""" """Convert django model to dict and return it."""
return { return {
"id": self.id, 'id': self.id,
"item": self.item.get_dict(), 'item': self.item.get_dict(),
"duration": self.duration, 'duration': self.duration,
} }
def __str__(self): def __str__(self):
"""Return model name.""" """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 Rotation(models.Model):
"""Class for rotation model.""" """Class for rotation model."""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
def get_dict(self): def get_dict(self):
@@ -367,20 +372,21 @@ class Rotation(models.Model):
# to avoid excluding items with no expire_date) # to avoid excluding items with no expire_date)
now = timezone.now() now = timezone.now()
instances = self.instances.all() 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)) instance_list = list(map(lambda i: i.get_dict(), filtered))
return { return {
"id": self.id, 'id': self.id,
"name": self.name, 'name': self.name,
"instances": instance_list, 'instances': instance_list,
} }
def get_list(self): def get_list(self):
"""Return list containing infoitem data.""" """Return list containing infoitem data."""
return { return {
"id": self.id, 'id': self.id,
"name": self.name, 'name': self.name,
} }
def __str__(self): def __str__(self):
@@ -391,7 +397,6 @@ class Rotation(models.Model):
class ImageUploadForm(forms.Form): class ImageUploadForm(forms.Form):
"""Form used to handle imageuploads to infoscreen app.""" """Form used to handle imageuploads to infoscreen app."""
id = models.AutoField(primary_key=True)
name = forms.CharField() name = forms.CharField()
image = forms.ImageField() image = forms.ImageField()
@@ -399,6 +404,5 @@ class ImageUploadForm(forms.Form):
class UploadFileForm(forms.Form): class UploadFileForm(forms.Form):
"""Form used for uploading file.""" """Form used for uploading file."""
id = models.AutoField(primary_key=True)
name = forms.CharField() name = forms.CharField()
video = forms.FileField() video = forms.FileField()
@@ -2,10 +2,7 @@ body {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
} }
#header:after { #header {
content: " ";
display: block;
clear: both;
} }
#header-logo { #header-logo {
@@ -12,7 +12,7 @@ body {
.event { .event {
font-size: 100px; font-size: 100px;
font-weight: bold; font-weight: bold;
margin-left: 20px; margin-left: 20px;
} }
.event-col{ .event-col{
padding-top:1vh; padding-top:1vh;
@@ -21,7 +21,7 @@ body {
.header-row{ .header-row{
margin: 30px; margin: 30px;
margin-left: 20px; margin-left: 20px;
font-size: 130px; font-size: 130px;
padding-bottom:20px; padding-bottom:20px;
color:#24a05f; 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 { #infocontent {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: fixed; position: fixed;
left: 0px; left: 0px;
top: 0px; top: 0px;
z-index: -1; /* Ensure div tag stays behind content; -999 might work, too. */ z-index: -1; /* Ensure div tag stays behind content; -999 might work, too. */
} }
@@ -5,7 +5,6 @@ html {
body { body {
padding: 1.5rem; padding: 1.5rem;
margin: 0.5rem; margin: 0.5rem;
height: 100%;
} }
tbody { tbody {
@@ -50,18 +49,7 @@ td {
} }
} }
#header { #header {
max-width: 100%; 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 { .article-thumb-col {
max-height: 200px; max-height: 200px;
text-align: left; text-align: right;
} }
.article-title-col { .article-title-col {
@@ -31,10 +31,10 @@
max-height 200px; max-height 200px;
} }
#sossoimage { #sossoimage {
height:300px; height:300px;
position: relative; position: relative;
left: 0px; left: 0px;
top: 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"> <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<div ng-controller="ABBController"> <div ng-controller="ABBController">
<!-- Only show the job listing if there are any jobs, i.e, the jobs list is non-empty --> <!-- 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" class="row" ng-if="jobs.length > 0">
<div id="header-logo"> <div id="header-logo">
<img src="/static/infoscreen/img/ABB_logo.png"> <img src="/static/img/ABB_logo.png">
</div> </div>
<div id="header-title"> <div id="header-title">
TYÖPAIKAT TYÖPAIKAT
@@ -28,6 +28,6 @@
<!-- If there are no jobs, show a static image --> <!-- If there are no jobs, show a static image -->
<div class="row" ng-if="jobs.length == 0"> <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>
</div> </div>
@@ -4,7 +4,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input class="form-control" type="text" ng-model="item.name"></input> <input type="text" ng-model="item.name"></input>
</div> </div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input> <input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div> </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"> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<div id="bg"> <div id="bg">
<div class="container " ng-controller="ApyController"> <div class="container " ng-controller="ApyController">
@@ -1,10 +1,10 @@
<div ng-controller="infoadmin_apyitem_create" style="margin-top:20px;"> <div ng-controller="infoadmin_apyitem_create" style="margin-top:20px;">
<div> <div>
Create new ÄPY statistics item create apyitem
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input class="form-control" type="text" ng-model="item.name"></input> <input type="text" ng-model="item.name"></input>
</div> </div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input> <input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div> </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>
@@ -1,5 +1,5 @@
<link rel="stylesheet" href="/static/infoscreen/css/events.css"> <link rel="stylesheet" href="/static/css/events.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<div class="container" ng-app="myApp" ng-controller="EventController"> <div class="container" ng-app="myApp" ng-controller="EventController">
<div class="header-row row"> <div class="header-row row">
<div class="col-sm-6">Tapahtuma</div> <div class="col-sm-6">Tapahtuma</div>
@@ -4,7 +4,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input> <input type="text" ng-model="item.name"></input>
</div> </div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input> <input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div> </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"> <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> <p>Your browser does not support iframes.</p>
</iframe> </iframe>
@@ -1,14 +1,14 @@
<div ng-controller="infoadmin_websiteitem_create" style="margin-top:20px;"> <div ng-controller="infoadmin_websiteitem_create" style="margin-top:20px;">
<div> <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>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input> <input type="text" ng-model="item.name"></input>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Url:</label> <label>Url:</label>
<input type="text" class="form-control" ng-model="item.url"></input> <input type="text" ng-model="item.url"></input>
</div> </div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input> <input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div> </div>
@@ -4,11 +4,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input> <input type="text" ng-model="item.name"></input>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Url:</label> <label>Url:</label>
<input type="text" class="form-control" ng-model="item.url"></input> <input type="text" ng-model="item.url"></input>
</div> </div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input> <input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div> </div>
@@ -4,7 +4,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input type="text" class="form-control" ng-model="imagename"></input> <input type="text" ng-model="imagename"></input>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="file" ngf-select ng-model="img" name="file" required> <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"> <div class="fullscreen-bg">
@@ -4,7 +4,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input type="text" class="form-control" ng-model="name"></input> <input type="text" ng-model="name"></input>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="file" ngf-select ng-model="video" name="file" required> <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: ['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>
+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>
<div class="form-group"> <div class="form-group">
<label>Name:</label> <label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input> <input type="text" ng-model="item.name"></input>
</div> </div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input> <input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div> </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>
@@ -1,17 +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/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>
@@ -182,8 +182,8 @@ var simple_controllers = [
"external_image", "external_image",
"abbitem", "abbitem",
"sossoitem", "sossoitem",
"lunchitem",
"eventitem", "eventitem",
"hslitem",
"websiteitem", "websiteitem",
"apyitem", "apyitem",
]; ];
@@ -46,18 +46,10 @@ 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){ app.controller('ABBController', function($scope, $http){
$scope.jobs = []; $scope.jobs = [];
var min_date = moment().subtract(30,'days').format("YYYY-MM-DD%20HH:mm:ss"); var min_date = moment().subtract(30,'days').format("YYYY-MM-DD%20HH:mm:ss");
// TODO: FIX, we try to get rid of php, not depend on it! var url = "https://sahkoinsinoorikilta.fi/api/news.php";
var url = "https://old.sahkoinsinoorikilta.fi/api/news.php";
var params = "?type=11&lang=fi&title_search=ABB&min_date="+min_date var params = "?type=11&lang=fi&title_search=ABB&min_date="+min_date
$http.get(url+params).then(function(response){ $http.get(url+params).then(function(response){
$scope.jobs = _.filter(response.data, function(job){ $scope.jobs = _.filter(response.data, function(job){
@@ -83,21 +75,6 @@ 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) { app.controller('ApyController', function($scope, $http) {
$scope.items = []; $scope.items = [];
$http.get("/infoscreen/apyjson").then(function(response) $http.get("/infoscreen/apyjson").then(function(response)
@@ -135,3 +112,35 @@ app.filter('unixTimeToDifference', function() {
return res; 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();
}
);
+154
View File
@@ -0,0 +1,154 @@
{% 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>
</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>
</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>
@@ -0,0 +1,29 @@
{% load static %}
<!DOCTYPE html>
<html ng-app="infoApp">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<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>
+1 -1
View File
@@ -35,6 +35,6 @@ class InfoscreenTestCase(TestCase):
That would mean that something meaningful has been included That would mean that something meaningful has been included
in the response. in the response.
""" """
resp = self.c.get("/infoscreen/items") resp = self.c.get('/infoscreen/items')
content = resp.json() content = resp.json()
self.assertTrue(len(content) > 0) self.assertTrue(len(content) > 0)
+29 -30
View File
@@ -1,7 +1,6 @@
"""File containing infoscreen urls.""" """File containing infoscreen urls."""
from django.urls import re_path from django.conf.urls import url
from django.conf import settings
from infoscreen.views import index from infoscreen.views import index
from infoscreen.views import admin from infoscreen.views import admin
@@ -18,40 +17,40 @@ from infoscreen.views import create_image_item
from infoscreen.views import create_video_item from infoscreen.views import create_video_item
from infoscreen.views import createABBItem from infoscreen.views import createABBItem
from infoscreen.views import createSossoItem from infoscreen.views import createSossoItem
from infoscreen.views import createLunchItem from infoscreen.views import createHslItem
from infoscreen.views import createEventItem from infoscreen.views import createEventItem
from infoscreen.views import createExternalWebsiteItem from infoscreen.views import createExternalWebsiteItem
from infoscreen.views import create_rotation from infoscreen.views import create_rotation
from infoscreen.views import delete_rotation from infoscreen.views import delete_rotation
from infoscreen.views import CurrentHSLView
from infoscreen.views import createApyItem from infoscreen.views import createApyItem
from infoscreen.views import hsl_timetable_settings
from infoscreen.views import get_apy_json from infoscreen.views import get_apy_json
urlpatterns = [ urlpatterns = [
re_path(r"^$", default), url(r'^$', default),
re_path(r"^admin$", admin), url(r'^admin$', admin),
re_path(r"^(?P<idx>\d+)$", index), url(r'^(?P<idx>\d+)$', index),
re_path(r"^items$", info_items), url(r'^items$', info_items),
re_path(r"^rotation/(?P<idx>\d+)$", rotation), url(r'^rotation/(?P<idx>\d+)$', rotation),
re_path(r"^rotations$", rotations), url(r'^rotations$', rotations),
re_path(r"^instance$", createInstance), url(r'^instance$', createInstance),
re_path(r"^instance/(?P<idx>\d+)$", deleteInstance), url(r'^instance/(?P<idx>\d+)$', deleteInstance),
re_path(r"^types$", info_types), url(r'^types$', info_types),
re_path(r"^delete_item/(?P<type_id>\d+)/(?P<idx>\d+)$", delete_info_item), url(r'^delete_item/(?P<type_id>\d+)/(?P<idx>\d+)$', delete_info_item),
re_path(r"^create_external_image$", createExternalImageInfoItem), url(r'^create_external_image$', createExternalImageInfoItem),
re_path(r"^create_image$", create_image_item), url(r'^create_image$', create_image_item),
re_path(r"^create_video$", create_video_item), url(r'^create_video$', create_video_item),
re_path(r"^create_abbitem$", createABBItem), url(r'^create_abbitem$', createABBItem),
re_path(r"^create_sossoitem$", createSossoItem), url(r'^create_sossoitem$', createSossoItem),
re_path(r"^create_lunchitem$", createLunchItem), url(r'^create_eventitem$', createEventItem),
re_path(r"^create_eventitem$", createEventItem), url(r'^create_hslitem$', createHslItem),
re_path(r"^create_apyitem$", createApyItem), url(r'^create_apyitem$', createApyItem),
re_path(r"^create_websiteitem$", createExternalWebsiteItem), url(r'^create_websiteitem$', createExternalWebsiteItem),
re_path(r"^create_rotation$", create_rotation), url(r'^create_rotation$', create_rotation),
re_path(r"^delete_rotation/(?P<id>\d+)$", delete_rotation), url(r'^delete_rotation/(?P<id>\d+)$', delete_rotation),
re_path(r"^apyjson", get_apy_json), 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()
+46 -58
View File
@@ -6,7 +6,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.decorators import permission_required, login_required from django.contrib.auth.decorators import permission_required, login_required
from django.db import DatabaseError
from infoscreen.models import UploadFileForm from infoscreen.models import UploadFileForm
import sikweb.settings as settings import sikweb.settings as settings
@@ -15,28 +14,21 @@ import logging
import threading import threading
import requests import requests
from infoscreen.models import ( from infoscreen.models import Rotation, InfoItem, InfoInstance
Rotation, from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem,
InfoItem, ImageInfoItem, SossoInfoItem, HslInfoItem)
InfoInstance, from infoscreen.models import EventInfoItem
ABBInfoItem, from infoscreen.models import ExternalWebsiteInfoItem
ExternalImageInfoItem, from infoscreen.models import ImageUploadForm
ImageInfoItem, from infoscreen.models import ApyInfoItem
SossoInfoItem, from infoscreen.models import VideoInfoItem
LunchItem,
EventInfoItem,
ExternalWebsiteInfoItem,
ImageUploadForm,
ApyInfoItem,
VideoInfoItem,
)
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.change_infoinstance", raise_exception=True) @permission_required('infoscreen.change_infoinstance', raise_exception=True)
def admin(request, *args, **kwargs): def admin(request, *args, **kwargs):
"""Render infoscreen admin page.""" """Render infoscreen admin page."""
return render(request, "infoscreen/infoscreen_admin.html", {}) return render(request, 'infoscreen_admin.html', {})
def create_item_generator(model): def create_item_generator(model):
@@ -44,23 +36,20 @@ def create_item_generator(model):
@ensure_csrf_cookie @ensure_csrf_cookie
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.add_infoinstance", raise_exception=True) @permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_item(request, *args, **kwargs): def create_item(request, *args, **kwargs):
try: try:
data = json.loads(request.body.decode("utf-8")) data = json.loads(request.body.decode("utf-8"))
except json.JSONDecodeError: except ValueError:
return HttpResponseBadRequest( return HttpResponseBadRequest(
'{"status":"failure","error":"invalid json supplied"}' '{"status":"failure","error":"invalid json supplied"}')
)
try: try:
model.create_from_dict(data) model.create_from_dict(data)
return HttpResponse('{"status":"success"}') return HttpResponse('{"status":"success"}')
except RuntimeError as e: except RuntimeError as e:
return HttpResponseBadRequest( return HttpResponseBadRequest(
json.dumps({"status": "failure", "error": str(e)}) json.dumps({"status": "failure", "error": str(e)}))
)
return create_item return create_item
@@ -69,8 +58,8 @@ def delete_item_generator(model):
@ensure_csrf_cookie @ensure_csrf_cookie
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.delete_infoinstance", raise_exception=True) @permission_required('infoscreen.delete_infoinstance', raise_exception=True)
def delete_item(request, *args, **kwargs): def delete_item(request, *args, **kwargs):
idx = kwargs.pop("idx", 0) idx = kwargs.pop("idx", 0)
try: try:
@@ -82,18 +71,17 @@ def delete_item_generator(model):
try: try:
item.delete() item.delete()
return HttpResponse('{"status":"success"}') return HttpResponse('{"status":"success"}')
except DatabaseError: except:
resp = HttpResponse('{"error" : "could not delete item"}') resp = HttpResponse('{"error" : "could not delete item"}')
resp.status_code = 500 resp.status_code = 500
return resp return resp
return delete_item return delete_item
# due to model structure this is little complicated # due to model structure this is little complicated
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.delete_infoinstance", raise_exception=True) @permission_required('infoscreen.delete_infoinstance', raise_exception=True)
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
def delete_info_item(request, *args, **kwargs): def delete_info_item(request, *args, **kwargs):
"""Delete info item.""" """Delete info item."""
@@ -109,7 +97,7 @@ def delete_info_item(request, *args, **kwargs):
try: try:
item.delete() item.delete()
return HttpResponse('{"status":"success"}') return HttpResponse('{"status":"success"}')
except DatabaseError: except:
resp = HttpResponse('{"error" : "could not delete item"}') resp = HttpResponse('{"error" : "could not delete item"}')
resp.status_code = 500 resp.status_code = 500
return resp return resp
@@ -117,65 +105,64 @@ def delete_info_item(request, *args, **kwargs):
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.add_infoinstance", raise_exception=True) @permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_image_item(request, *args, **kwargs): def create_image_item(request, *args, **kwargs):
"""Create image Infoscreen item.""" """Create image Infoscreen item."""
form = ImageUploadForm(request.POST, request.FILES) form = ImageUploadForm(request.POST, request.FILES)
if not form.is_valid(): if not form.is_valid():
return HttpResponseBadRequest( return HttpResponseBadRequest('{"status": "failure",'
'{"status": "failure",' '"error": "invalid data supplied"}' '"error": "invalid data supplied"}')
)
img = form.cleaned_data["image"] img = form.cleaned_data['image']
name = form.cleaned_data["name"] name = form.cleaned_data['name']
ImageInfoItem.objects.create(img=img, name=name) ImageInfoItem.objects.create(img=img, name=name)
return HttpResponse('{"status":"success"}') return HttpResponse('{"status":"success"}')
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.add_infoinstance", raise_exception=True) @permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_video_item(request, *args, **kwargs): def create_video_item(request, *args, **kwargs):
"""Create video Infoscreen item.""" """Create video Infoscreen item."""
form = UploadFileForm(request.POST, request.FILES) form = UploadFileForm(request.POST, request.FILES)
if not form.is_valid(): if not form.is_valid():
return HttpResponseBadRequest( return HttpResponseBadRequest('{"status": "failure",'
'{"status": "failure",' '"error": "invalid data supplied"}' '"error": "invalid data supplied"}')
)
video = form.cleaned_data["video"] video = form.cleaned_data['video']
name = form.cleaned_data["name"] name = form.cleaned_data['name']
VideoInfoItem.objects.create(video=video, name=name) VideoInfoItem.objects.create(video=video, name=name)
return HttpResponse('{"status": "success"}') return HttpResponse('{"status": "success"}')
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.add_rotation", raise_exception=True) @permission_required('infoscreen.add_rotation', raise_exception=True)
def create_rotation(request, *args, **kwargs): def create_rotation(request, *args, **kwargs):
"""Create rotation.""" """Create rotation."""
try: try:
data = json.loads(request.body.decode("utf-8")) data = json.loads(request.body.decode("utf-8"))
except json.JSONDecodeError: except:
return HttpResponse('{"error": "bad post body!"}', status=400) return HttpResponse('{"error": "bad post body!"}', status=400)
try: try:
name = data["name"] name = data["name"]
Rotation.objects.create(name=name) Rotation.objects.create(name=name)
resp = HttpResponse(status=200) resp = HttpResponse(status=200)
except DatabaseError: except:
resp = HttpResponse('{"error" : "could not create rotation!"}', status=400) resp = HttpResponse(
'{"error" : "could not create rotation!"}', status=400)
return resp return resp
@require_http_methods(["DELETE"]) @require_http_methods(["DELETE"])
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required(login_url="/admin/login") @login_required(login_url='/login')
@permission_required("infoscreen.delete_rotation", raise_exception=True) @permission_required('infoscreen.delete_rotation', raise_exception=True)
def delete_rotation(request, *args, **kwargs): def delete_rotation(request, *args, **kwargs):
"""Delete rotation.""" """Delete rotation."""
id = kwargs.pop("id", 0) id = kwargs.pop("id", 0)
@@ -184,8 +171,9 @@ def delete_rotation(request, *args, **kwargs):
try: try:
Rotation.objects.filter(id=id).delete() Rotation.objects.filter(id=id).delete()
resp = HttpResponse(status=200) resp = HttpResponse(status=200)
except DatabaseError: except:
resp = HttpResponse('{"error" : "could not delete rotation!"}', status=400) resp = HttpResponse(
'{"error" : "could not delete rotation!"}', status=400)
return resp return resp
@@ -194,7 +182,7 @@ createInstance = create_item_generator(InfoInstance)
deleteInstance = delete_item_generator(InfoInstance) deleteInstance = delete_item_generator(InfoInstance)
createABBItem = create_item_generator(ABBInfoItem) createABBItem = create_item_generator(ABBInfoItem)
createSossoItem = create_item_generator(SossoInfoItem) createSossoItem = create_item_generator(SossoInfoItem)
createLunchItem = create_item_generator(LunchItem) createHslItem = create_item_generator(HslInfoItem)
createExternalImageInfoItem = create_item_generator(ExternalImageInfoItem) createExternalImageInfoItem = create_item_generator(ExternalImageInfoItem)
createExternalWebsiteItem = create_item_generator(ExternalWebsiteInfoItem) createExternalWebsiteItem = create_item_generator(ExternalWebsiteInfoItem)
createEventItem = create_item_generator(EventInfoItem) createEventItem = create_item_generator(EventInfoItem)
+30 -12
View File
@@ -2,20 +2,19 @@ from django.shortcuts import render
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.conf import settings from django.conf import settings
from django.db import DatabaseError
from infoscreen.models import Rotation, InfoItem, InfoInstance from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.hsl_fetcher import fetch as hsl_fetch
import json import json
import logging import logging
import threading import threading
import requests
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def index(request, idx, *args, **kwargs): def index(request, idx, *args, **kwargs):
"""Render infoscreen index page.""" """Render infoscreen index page."""
return render(request, "infoscreen/infoscreen_index.html", {"rotation": idx}) return render(request, 'infoscreen_index.html', {'rotation': idx})
@require_http_methods(["GET"]) @require_http_methods(["GET"])
@@ -23,7 +22,7 @@ def default(request, *args, **kwargs):
"""Try getting first rotation item.""" """Try getting first rotation item."""
try: try:
first = Rotation.objects.all()[0].id first = Rotation.objects.all()[0].id
except DatabaseError: except:
first = 0 first = 0
return index(request, first, *args, **kwargs) return index(request, first, *args, **kwargs)
@@ -32,8 +31,7 @@ def default(request, *args, **kwargs):
def get_apy_json(request): def get_apy_json(request):
"""Render APY diilikone page.""" """Render APY diilikone page."""
return HttpResponse( return HttpResponse(
requests.get("https://api-diilikone.apy.fi/deals/top-groups").text requests.get("https://api-diilikone.apy.fi/deals/top-groups").text)
)
@require_http_methods(["GET"]) @require_http_methods(["GET"])
@@ -62,12 +60,10 @@ def info_types(request, *args, **kwargs):
types = [] types = []
classes = InfoItem.get_subclasses() classes = InfoItem.get_subclasses()
for c in classes: for c in classes:
types.append( types.append({
{ "name": c.display_name,
"name": c.display_name, "create_template_url": c.get_create_template_url(),
"create_template_url": c.get_create_template_url(), })
}
)
return HttpResponse(json.dumps(types)) return HttpResponse(json.dumps(types))
@@ -81,3 +77,25 @@ def info_items(request, *args, **kwargs):
items.append(i.get_dict()) items.append(i.get_dict())
return JsonResponse(items, safe=False) 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)
-9
View File
@@ -1,9 +0,0 @@
from django.contrib import admin
from modeltranslation.admin import TranslationAdmin
from kaehmy.models import Application, Comment, CustomRole, PresetRole
admin.site.register(Application)
admin.site.register(Comment)
admin.site.register(CustomRole)
admin.site.register(PresetRole, TranslationAdmin)
-5
View File
@@ -1,5 +0,0 @@
from django.apps import AppConfig
class KaehmyConfig(AppConfig):
name = "kaehmy"

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