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]
omit =
*/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
.env
*.swp
sikweb/settings.py
*~
*.pyc
/collected_static/
/media/
logs/
*.sqlite3
uwsgi.ini
uwsgi.log
infoscreen/static/js/hsl.json
members/logs/*
node_modules/
.coverage
.vscode/
.idea/
*.code-workspace
venv/
.venv/
poetry.lock
logs/
/media/
node_modules/
/.coverage
db.sqlite3
requirements_henu.txt
/collected_static/
mydatabase
settings.json
.vscode/
+90 -181
View File
@@ -1,191 +1,100 @@
stages:
- setup
- audit
- lint
- test
- publish
- deploy
- cleanup
install:
image: node:22
stage: setup
only:
- pushes
script:
- npm ci
artifacts:
paths:
- node_modules
expire_in: 1 week
audit:
image: python:3.12.9
stage: audit
allow_failure: true
only:
- pushes
needs: []
before_script:
- pip install pip==25.3
- pip install poetry==2.1.1
- poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi
script:
- safety check
- test
- lint
- publish
- deploy
test:
image: python:3.12.9
stage: test
only:
- pushes
needs: []
services:
- postgres:12
variables:
POSTGRES_DB: ci
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
DB_HOST: postgres
before_script:
- pip install pip==25.3
- pip install poetry==2.1.1
- poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi
script:
- python manage.py migrate --noinput
- python manage.py createdefaultadmin
- python manage.py test
image: python:3.5
stage: test
services:
- postgres:latest
variables:
POSTGRES_DB: ci
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
script:
- python -V
- pip install -r requirements.txt
- cp sikweb/settings-sample.py sikweb/default_settings.py
- cp sikweb/.ci-settings.py sikweb/settings.py
- python manage.py migrate --noinput
- python manage.py createdefaultadmin
- python manage.py test
lint:py:
image: python:3.12.9
stage: lint
only:
- pushes
needs: []
script:
- pip install black==22.3.0
- black --check .
pycodestyle:
image: python:3.5
stage: lint
script:
- pip install pycodestyle
- pycodestyle --config=setup.cfg --count .
lint:js:
image: node:22
stage: lint
only:
- pushes
needs: ["install"]
script:
- npm run lint:js
eslint:
image: node:7.10.0
stage: lint
before_script:
- npm install
script:
- npm run eslint
lint:md:
image: node:22
stage: lint
only:
- pushes
needs: ["install"]
script:
- npm run lint:md
remark:
image: node:7.10.0
stage: lint
before_script:
- npm install
script:
- npm run remark
publish:
image: docker:25-cli
stage: publish
needs: ["test", "lint:py", "lint:js", "lint:md"]
services:
- docker:25-dind
only:
- main
- production
script:
- docker info
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build . -t "$IMAGE_NAME"
- docker push "$IMAGE_NAME"
stage: publish
image: docker:latest
only:
- develop
before_script:
- docker info
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY_URL
script:
- docker build . -t "$IMAGE_NAME"
- docker push "$IMAGE_NAME"
deploy:dev:
image: docker:25-cli
stage: deploy
only:
- main
environment:
name: dev
url: http://api.dev.sahkoinsinoorikilta.fi
variables:
DOCKER_HOST: $DEV_CI_DOCKER_HOST
DOCKER_TLS_VERIFY: 1
before_script:
- mkdir -p ~/.docker
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
after_script:
- docker logout "$CI_REGISTRY"
deploy_dev:
stage: deploy
image: alpine:latest
environment:
name: dev
url: http://web.sik.party:8080
only:
- develop
before_script:
- pwd
- apk add --update openssh
- ssh -V
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
script:
- scp docker-compose.yml $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/docker-compose.yml
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "cd deployment && docker-compose down && docker pull \"$IMAGE_NAME\" && docker-compose up -d && docker image prune -f"
deploy:production:
stage: deploy
image: docker:25-cli
only:
- production
environment:
name: production
url: https://api.sahkoinsinoorikilta.fi
when: manual
variables:
DOCKER_HOST: $CI_DOCKER_HOST
DOCKER_TLS_VERIFY: 1
before_script:
- mkdir -p ~/.docker
- echo "$TLSCACERT" > ~/.docker/ca.pem
- echo "$TLSCERT" > ~/.docker/cert.pem
- echo "$TLSKEY" > ~/.docker/key.pem
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
after_script:
- docker logout "$CI_REGISTRY"
docker_prune:dev:
image: docker:stable
stage: cleanup
only:
- schedules
environment:
name: dev
url: http://api.dev.sahkoinsinoorikilta.fi
variables:
DOCKER_HOST: $DEV_CI_DOCKER_HOST
DOCKER_TLS_VERIFY: 1
before_script:
- mkdir -p ~/.docker
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker system prune
after_script:
- docker logout "$CI_REGISTRY"
docker_prune:prod:
image: docker:stable
stage: cleanup
only:
- schedules
environment:
name: production
url: https://api.sahkoinsinoorikilta.fi
variables:
DOCKER_HOST: $CI_DOCKER_HOST
DOCKER_TLS_VERIFY: 1
before_script:
- mkdir -p ~/.docker
- echo "$TLSCACERT" > ~/.docker/ca.pem
- echo "$TLSCERT" > ~/.docker/cert.pem
- echo "$TLSKEY" > ~/.docker/key.pem
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- docker system prune
after_script:
- docker logout "$CI_REGISTRY"
deploy_production:
stage: deploy
image: alpine:latest
environment:
name: production
url: https://sika.sahkoinsinoorikilta.fi
when: manual
only:
- master
before_script:
- pwd
- apk add --update openssh
- ssh -V
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
script:
- ssh $PROD_SSH_USER@$PROD_SSH_HOST "zsh ~/deploy.sh"
-12
View File
@@ -1,12 +0,0 @@
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
. "${VIRTUAL_ENV}/bin/activate"
if [ $? -ne 0 ]
then
printf "${PURPLE}Failed to find virtualenv. Skipping pre-commit hook.\n${NC}"
exit 0
fi
npm run lint
-21
View File
@@ -1,21 +0,0 @@
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
. "${VIRTUAL_ENV}/bin/activate"
if [ $? -ne 0 ]
then
printf "${PURPLE}Failed to find virtualenv. Skipping pre-push hook.\n${NC}"
exit 0
fi
set -e
printf "${PURPLE}Running pre-push tests.${NC}\n"
printf "${PURPLE}linters...${NC}\n"
npm run lint
printf "${PURPLE}unit tests...${NC}\n"
python -Wall manage.py test --noinput
set +e
printf "${PURPLE}Tests passed.${NC}\n"
-1
View File
@@ -1 +0,0 @@
22.13.1
-1
View File
@@ -1 +0,0 @@
3.12.9
+1
View File
@@ -0,0 +1 @@
global_static
-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
COPY . ./
ENV POETRY_VERSION=2.1.1
RUN pip install pip==25.3
RUN pip install "poetry==$POETRY_VERSION"
RUN poetry self add poetry-plugin-export
RUN poetry export --without-hashes --format=requirements.txt --output requirements.txt
FROM python:3.12.9-slim-bullseye AS server
WORKDIR /app
COPY . ./
COPY --from=builder requirements.txt ./
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
# pip
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
RUN apt-get update && apt-get install --no-install-recommends -y build-essential
RUN pip install pip==25.3
RUN pip install --no-deps -r requirements.txt
RUN python manage.py collectstatic --noinput
CMD ["sh", "-c", "./production_entrypoint.sh"]
ENV IS_DOCKER 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN env
ADD . /code/
-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:
db:
image: postgres:12
volumes:
- dbdata:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=postgres
image: postgres
web:
build: .
environment:
- DEPLOY_ENV=local
- 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='{}'
image: 86.50.143.82:5000/web20
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8080"]
ports:
- 8000:8000
- "8080:8080"
depends_on:
- db
volumes:
dbdata:
-23
View File
@@ -1,23 +0,0 @@
import globals from "globals";
import js from "@eslint/js";
export default [
{
ignores: ["**/.venv/", "**/collected_static/", "**/static/js/lib/**"],
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.jquery,
angular: true,
moment: true,
_: true
},
},
},
{
...js.configs.recommended
}
];
+4 -10
View File
@@ -1,16 +1,10 @@
"""Admin site registers."""
from django.contrib import admin
from infoscreen.models import (
Rotation,
InfoItem,
InfoInstance,
ImageInfoItem,
ExternalImageInfoItem,
ABBInfoItem,
ExternalWebsiteInfoItem,
VideoInfoItem,
)
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem
from infoscreen.models import ExternalWebsiteInfoItem
from infoscreen.models import VideoInfoItem
# Register your models here.
admin.site.register(Rotation)
+1 -1
View File
@@ -6,4 +6,4 @@ from django.apps import AppConfig
class InfoscreenConfig(AppConfig):
"""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
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name="HSLDataModel",
name='HSLDataModel',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("data", models.TextField(default="", editable=False)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.TextField(default='', editable=False)),
],
),
migrations.CreateModel(
name="InfoInstance",
name='InfoInstance',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("duration", models.FloatField(default=15.0)),
("item_id", models.PositiveIntegerField()),
(
"item_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.ContentType",
),
),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('duration', models.FloatField(default=15.0)),
('item_id', models.PositiveIntegerField()),
('item_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.CreateModel(
name="InfoItem",
name='InfoItem',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("expire_date", models.DateTimeField(blank=True, null=True)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('expire_date', models.DateTimeField(blank=True, null=True)),
],
),
migrations.CreateModel(
name="Rotation",
name='Rotation',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="ABBInfoItem",
name='ABBInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="ExternalImageInfoItem",
name='ExternalImageInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
("url", models.TextField()),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
('url', models.TextField()),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="HslInfoItem",
name='HslInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="ImageInfoItem",
name='ImageInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
("img", models.ImageField(upload_to="infoimages/")),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
('img', models.ImageField(upload_to='infoimages/')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="SossoInfoItem",
name='SossoInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.AddField(
model_name="infoinstance",
name="rotation",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="instances",
to="infoscreen.Rotation",
),
model_name='infoinstance',
name='rotation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='infoscreen.Rotation'),
),
]
+4 -14
View File
@@ -9,25 +9,15 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0001_initial"),
('infoscreen', '0001_initial'),
]
operations = [
migrations.CreateModel(
name="CoffeeInfoItem",
name='CoffeeInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
]
@@ -9,63 +9,33 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0002_coffeeinfoitem"),
('infoscreen', '0002_coffeeinfoitem'),
]
operations = [
migrations.CreateModel(
name="ApyInfoItem",
name='ApyInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="EventInfoItem",
name='EventInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="ExternalWebsiteInfoItem",
name='ExternalWebsiteInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
("url", models.TextField()),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
('url', models.TextField()),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.DeleteModel(
name="CoffeeInfoItem",
name='CoffeeInfoItem',
),
]
+5 -15
View File
@@ -9,26 +9,16 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0003_auto_20170329_1857"),
('infoscreen', '0003_auto_20170329_1857'),
]
operations = [
migrations.CreateModel(
name="VideoInfoItem",
name='VideoInfoItem',
fields=[
(
"infoitem_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="infoscreen.InfoItem",
),
),
("video", models.FileField(upload_to="infovideos/")),
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
('video', models.FileField(upload_to='infovideos/')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
]
@@ -8,18 +8,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0004_videoinfoitem"),
('infoscreen', '0004_videoinfoitem'),
]
operations = [
migrations.AlterField(
model_name="externalimageinfoitem",
name="url",
model_name='externalimageinfoitem',
name='url',
field=models.URLField(),
),
migrations.AlterField(
model_name="externalwebsiteinfoitem",
name="url",
model_name='externalwebsiteinfoitem',
name='url',
field=models.URLField(),
),
]
@@ -8,14 +8,11 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0005_auto_20170913_1841"),
('infoscreen', '0005_auto_20170913_1841'),
]
operations = [
migrations.DeleteModel(
name="HSLDataModel",
),
migrations.DeleteModel(
name="HslInfoItem",
name='HSLDataModel',
),
]
-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.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
class InfoItem(models.Model):
@@ -16,7 +16,6 @@ class InfoItem(models.Model):
class __meta__:
abstract = True
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
# expire_date = None means never expiring item
expire_date = models.DateTimeField(blank=True, null=True)
@@ -24,14 +23,14 @@ class InfoItem(models.Model):
def get_template_url(self):
"""Get infoscreen template url."""
raise NotImplementedError("inheriting classes must implement get_template_url")
raise NotImplementedError(
"inheriting classes must implement get_template_url")
@staticmethod
def get_create_template_url():
"""Get create infoscreen template url command."""
raise NotImplementedError(
"inheriting classes must implement get_create_template_url"
)
"inheriting classes must implement get_create_template_url")
@classmethod
def create_from_dict(cls, d):
@@ -43,13 +42,14 @@ class InfoItem(models.Model):
def update_from_dict(self, d):
"""Update model based on given dict."""
try:
expire_date = d.pop("expire_date", None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except:
pass
dmap = {
"name": "name",
'name': 'name',
}
for k, v in d.items():
try:
@@ -61,13 +61,13 @@ class InfoItem(models.Model):
def get_dict(self):
"""Convert django model to dict and return it."""
return {
"id": self.id,
"name": self.name,
"item_type": ContentType.objects.get_for_model(self).id,
"template_url": self.get_template_url(),
"display_name": self.display_name,
"create_template_url": self.get_create_template_url(),
"options": {},
'id': self.id,
'name': self.name,
'item_type': ContentType.objects.get_for_model(self).id,
'template_url': self.get_template_url(),
'display_name': self.display_name,
'create_template_url': self.get_create_template_url(),
'options': {}
}
def delete(self):
@@ -75,8 +75,8 @@ class InfoItem(models.Model):
# since generic foreign keys suck, delete info
# items pointing here manually
InfoInstance.objects.filter(
item_id=self.id, item_type=ContentType.objects.get_for_model(self)
).delete()
item_id=self.id,
item_type=ContentType.objects.get_for_model(self)).delete()
super().delete()
@classmethod
@@ -98,12 +98,12 @@ class ABBInfoItem(InfoItem):
def get_template_url(self):
"""Return ABB infoitem template url."""
return "/static/infoscreen/html/abb.html"
return "/static/html/abb.html"
@staticmethod
def get_create_template_url():
"""Call create ABB infoitem template url command."""
return "/static/infoscreen/html/abb_create.html"
return "/static/html/abb_create.html"
class ApyInfoItem(InfoItem):
@@ -113,12 +113,12 @@ class ApyInfoItem(InfoItem):
def get_template_url(self):
"""Return APY infoitem template url."""
return "/static/infoscreen/html/apy.html"
return "/static/html/apy.html"
@staticmethod
def get_create_template_url():
"""Call create APY infoitem template url command."""
return "/static/infoscreen/html/apy_create.html"
return "/static/html/apy_create.html"
class ExternalWebsiteInfoItem(InfoItem):
@@ -129,17 +129,17 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_template_url(self):
"""Return external website infoitem template url."""
return "/static/infoscreen/html/external_website.html?url={}".format(self.name)
return "/static/html/external_website.html?url={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create external website infoitem template url command."""
return "/static/infoscreen/html/external_website_create.html"
return "/static/html/external_website_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"url": self.url}
d["options"] = {'url': self.url}
return d
@classmethod
@@ -152,22 +152,23 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_list(self):
"""Return list containing infoitem data."""
return {
"id": self.id,
"name": self.name,
"url": self.url,
'id': self.id,
'name': self.name,
'url': self.url,
}
def update_from_dict(self, d):
"""Update model based on given dict."""
try:
expire_date = d.pop("expire_date", None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except:
pass
dmap = {
"name": "name",
"url": "url",
'name': 'name',
'url': 'url',
}
for k, v in d.items():
try:
@@ -184,25 +185,12 @@ class SossoInfoItem(InfoItem):
def get_template_url(self):
"""Return Sosso infoitem template url."""
return "/static/infoscreen/html/sosso.html"
return "/static/html/sosso.html"
@staticmethod
def get_create_template_url():
"""Call create Sosso infoitem template url command."""
return "/static/infoscreen/html/sosso_create.html"
class LunchItem(InfoItem):
"""Class for Lunch Infoscreen item."""
display_name = _("Today's lunch")
def get_template_url(self):
return "/static/infoscreen/html/lunch.html"
@staticmethod
def get_create_template_url():
return "/static/infoscreen/html/lunch_create.html"
return "/static/html/sosso_create.html"
class EventInfoItem(InfoItem):
@@ -212,12 +200,12 @@ class EventInfoItem(InfoItem):
def get_template_url(self):
"""Return Event infoitem template url."""
return "/static/infoscreen/html/events.html"
return "/static/html/events.html"
@staticmethod
def get_create_template_url():
"""Call create Event infoitem template url command."""
return "/static/infoscreen/html/events_create.html"
return "/static/html/events_create.html"
class ImageInfoItem(InfoItem):
@@ -230,42 +218,57 @@ class ImageInfoItem(InfoItem):
"""Return Image infoitem template url."""
# get param to avoid angular from optimizing same template
# with different options
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
return "/static/html/generic_image.html?img={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create Image infoitem template url command."""
return "/static/infoscreen/html/generic_image_create.html"
return "/static/html/generic_image_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"img": self.img.url}
d["options"] = {'img': self.img.url}
return d
class VideoInfoItem(InfoItem):
"""Class for Video Infoscreen item."""
display_name = "Video"
display_name = ("Video")
video = models.FileField(upload_to="infovideos/")
def get_template_url(self):
"""Return Video infoitem template url."""
return "/static/infoscreen/html/generic_video.html?video={}".format(self.name)
return "/static/html/generic_video.html?video={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create Video infoitem template url command."""
return "/static/infoscreen/html/generic_video_create.html"
return "/static/html/generic_video_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"video": self.video.url}
d["options"] = {'video': self.video.url}
return d
class HslInfoItem(InfoItem):
"""Class for HSL Infoscreen item."""
display_name = _("HSL timetables")
def get_template_url(self):
"""Return HSL infoitem template url."""
return "/static/html/hsl.html"
@staticmethod
def get_create_template_url():
"""Call create HSL infoitem template url command."""
return "/static/html/hsl_create.html"
class ExternalImageInfoItem(InfoItem):
"""Class for External Image Infoscreen item."""
@@ -274,17 +277,17 @@ class ExternalImageInfoItem(InfoItem):
def get_template_url(self):
"""Return External Image infoitem template url."""
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
return "/static/html/generic_image.html?img={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create External Image infoitem template url command."""
return "/static/infoscreen/html/generic_external_image_create.html"
return "/static/html/generic_external_image_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"img": self.url}
d["options"] = {'img': self.url}
return d
@classmethod
@@ -297,14 +300,15 @@ class ExternalImageInfoItem(InfoItem):
def update_from_dict(self, d):
"""Update model based on given dict."""
try:
expire_date = d.pop("expire_date", None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except:
pass
dmap = {
"name": "name",
"url": "url",
'name': 'name',
'url': 'url',
}
for k, v in d.items():
try:
@@ -317,15 +321,12 @@ class ExternalImageInfoItem(InfoItem):
class InfoInstance(models.Model):
"""Class for Info instance in Infoscreen."""
id = models.AutoField(primary_key=True)
rotation = models.ForeignKey(
"Rotation", related_name="instances", on_delete=models.CASCADE
)
rotation = models.ForeignKey('Rotation', related_name='instances')
duration = models.FloatField(default=15.0) # seconds
# generic relation to some kind of InfoItem
item_id = models.PositiveIntegerField()
item_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
item = GenericForeignKey("item_type", "item_id")
item = GenericForeignKey('item_type', 'item_id')
@classmethod
def create_from_dict(cls, d):
@@ -338,27 +339,31 @@ class InfoInstance(models.Model):
except:
raise RuntimeError("invalid parameters supplied supplied")
try:
return cls.objects.create(rotation=rotation, item=item, duration=duration)
return cls.objects.create(
rotation=rotation,
item=item,
duration=duration
)
except:
raise RuntimeError("error while adding instance to db")
def get_dict(self):
"""Convert django model to dict and return it."""
return {
"id": self.id,
"item": self.item.get_dict(),
"duration": self.duration,
'id': self.id,
'item': self.item.get_dict(),
'duration': self.duration,
}
def __str__(self):
"""Return model name."""
return "{}: {} ({}s)".format(self.rotation.name, self.item.name, self.duration)
return "{}: {} ({}s)".format(
self.rotation.name, self.item.name, self.duration)
class Rotation(models.Model):
"""Class for rotation model."""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
def get_dict(self):
@@ -367,20 +372,21 @@ class Rotation(models.Model):
# to avoid excluding items with no expire_date)
now = timezone.now()
instances = self.instances.all()
filtered = filter(lambda i: (i.item.expire_date or now) >= now, list(instances))
filtered = filter(lambda i: (i.item.expire_date or now) >= now,
list(instances))
instance_list = list(map(lambda i: i.get_dict(), filtered))
return {
"id": self.id,
"name": self.name,
"instances": instance_list,
'id': self.id,
'name': self.name,
'instances': instance_list,
}
def get_list(self):
"""Return list containing infoitem data."""
return {
"id": self.id,
"name": self.name,
'id': self.id,
'name': self.name,
}
def __str__(self):
@@ -391,7 +397,6 @@ class Rotation(models.Model):
class ImageUploadForm(forms.Form):
"""Form used to handle imageuploads to infoscreen app."""
id = models.AutoField(primary_key=True)
name = forms.CharField()
image = forms.ImageField()
@@ -399,6 +404,5 @@ class ImageUploadForm(forms.Form):
class UploadFileForm(forms.Form):
"""Form used for uploading file."""
id = models.AutoField(primary_key=True)
name = forms.CharField()
video = forms.FileField()
@@ -2,10 +2,7 @@ body {
font-family: 'Open Sans', sans-serif;
}
#header:after {
content: " ";
display: block;
clear: both;
#header {
}
#header-logo {
@@ -12,7 +12,7 @@ body {
.event {
font-size: 100px;
font-weight: bold;
margin-left: 20px;
margin-left: 20px;
}
.event-col{
padding-top:1vh;
@@ -21,7 +21,7 @@ body {
.header-row{
margin: 30px;
margin-left: 20px;
margin-left: 20px;
font-size: 130px;
padding-bottom:20px;
color:#24a05f;
+74
View File
@@ -0,0 +1,74 @@
table {
font-size: 4vh;
font-family: 'Droid Sans Mono', monospace;
}
.red {
color: red;
-webkit-animation-name: blinker;
-webkit-animation-duration: 2s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-name: blinker;
-moz-animation-duration: 2s;
-moz-animation-timing-function: linear;
-moz-animation-iteration-count: infinite;
animation-name: blinker;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.black {
color: black;
}
@-moz-keyframes blinker {
0% { opacity: 1.0; }
50% { opacity: 0.1; }
100% { opacity: 1.0; }
}
@-webkit-keyframes blinker {
0% { opacity: 1.0; }
50% { opacity: 0.1; }
100% { opacity: 1.0; }
}
@keyframes blinker {
0% { opacity: 1.0; }
50% { opacity: 0.1; }
100% { opacity: 1.0; }
}
thead{
background: #f0f0f0;
}
.header-row{
background: #f0f0f0;
font-size: 7vh;
font-family: 'Droid Sans Mono', monospace;
text-align: center;
}
.container {
width: 100vw;
padding: 0 0 0 0;
}
.container .table {
margin-left: 0;
margin-right: 0;
}
.repeat-item.ng-leave {
}
.repeat-item.ng-leave.ng-leave-active {
opacity: 0;
font-size: 0vh;
}
.repeat-item.ng-leave{
opacity: 1;
font-size: 5vh;
}
@@ -1,8 +1,8 @@
#infocontent {
width: 100%;
height: 100%;
position: fixed;
left: 0px;
width: 100%;
height: 100%;
position: fixed;
left: 0px;
top: 0px;
z-index: -1; /* Ensure div tag stays behind content; -999 might work, too. */
}
@@ -5,7 +5,6 @@ html {
body {
padding: 1.5rem;
margin: 0.5rem;
height: 100%;
}
tbody {
@@ -50,18 +49,7 @@ td {
}
}
#header {
max-width: 100%;
display: flex;
justify-content: flex-end;
}
.tab-content {
margin-top: 1rem;
}
.rotation-title-row {
display: flex;
justify-content: space-between;
align-items: center;
}
@@ -19,7 +19,7 @@
.article-thumb-col {
max-height: 200px;
text-align: left;
text-align: right;
}
.article-title-col {
@@ -31,10 +31,10 @@
max-height 200px;
}
#sossoimage {
#sossoimage {
height:300px;
position: relative;
left: 0px;
position: relative;
left: 0px;
top: 0px;
}
@@ -1,10 +1,10 @@
<link rel="stylesheet" href="/static/infoscreen/css/abb.css">
<link rel="stylesheet" href="/static/css/abb.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<div ng-controller="ABBController">
<!-- Only show the job listing if there are any jobs, i.e, the jobs list is non-empty -->
<div id="header" class="row" ng-if="jobs.length > 0">
<div id="header-logo">
<img src="/static/infoscreen/img/ABB_logo.png">
<img src="/static/img/ABB_logo.png">
</div>
<div id="header-title">
TYÖPAIKAT
@@ -28,6 +28,6 @@
<!-- If there are no jobs, show a static image -->
<div class="row" ng-if="jobs.length == 0">
<img src="/static/infoscreen/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
<img src="/static/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
</div>
</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input class="form-control" type="text" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/infoscreen/css/apy.css">
<link rel="stylesheet" href="/static/css/apy.css">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<div id="bg">
<div class="container " ng-controller="ApyController">
@@ -1,10 +1,10 @@
<div ng-controller="infoadmin_apyitem_create" style="margin-top:20px;">
<div>
Create new ÄPY statistics item
create apyitem
</div>
<div class="form-group">
<label>Name:</label>
<input class="form-control" type="text" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
+4
View File
@@ -0,0 +1,4 @@
<link rel="stylesheet" href="/static/css/coffee.css">
<iframe src="https://host2.kilta.aalto.fi/kahvi/cups" allowfullscreen=true sandbox="allow-scripts allow-pointer-lock allow-same-origin">
<p>Your browser does not support iframes.</p>
</iframe>
@@ -1,5 +1,5 @@
<link rel="stylesheet" href="/static/infoscreen/css/events.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<link rel="stylesheet" href="/static/css/events.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<div class="container" ng-app="myApp" ng-controller="EventController">
<div class="header-row row">
<div class="col-sm-6">Tapahtuma</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/infoscreen/css/external_website.css">
<link rel="stylesheet" href="/static/css/external_website.css">
<iframe ng-src="{{ url | trusted_url }}" allowfullscreen=true sandbox="allow-scripts allow-pointer-lock allow-same-origin">
<p>Your browser does not support iframes.</p>
</iframe>
@@ -1,14 +1,14 @@
<div ng-controller="infoadmin_websiteitem_create" style="margin-top:20px;">
<div>
Create new item to show external website. For example "https://ka.dy.fi".
Create new item to show external website. For example "ka.dy.fi".
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<div class="form-group">
<label>Url:</label>
<input type="text" class="form-control" ng-model="item.url"></input>
<input type="text" ng-model="item.url"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -4,11 +4,11 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<div class="form-group">
<label>Url:</label>
<input type="text" class="form-control" ng-model="item.url"></input>
<input type="text" ng-model="item.url"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="imagename"></input>
<input type="text" ng-model="imagename"></input>
</div>
<div class="form-group">
<input type="file" ngf-select ng-model="img" name="file" required>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/infoscreen/css/video.css">
<link rel="stylesheet" href="/static/css/video.css">
<div class="fullscreen-bg">
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="name"></input>
<input type="text" ng-model="name"></input>
</div>
<div class="form-group">
<input type="file" ngf-select ng-model="video" name="file" required>
+41
View File
@@ -0,0 +1,41 @@
<link rel="stylesheet" href="/static/css/hsl.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/locale/fi.js"></script>
<div class="container" ng-app="myApp" ng-controller="timetableCtrl">
<div class="header-row row">
<div class="col-sm-2"><p>{{clock | date:'HH:mm'}}</p></div>
<div class="col-sm-8">HSL-Aikataulut</div>
<div class="col-sm-2 time"></div>
</div>
<h1 style="font-size: 10vh; text-align: center" ng-if="error">
{{error}}
</h1>
<table ng-if="!error" class="table table-striped row">
<thead>
<tr>
<th>
Aika
</th>
<th>
Linja
</th>
<th>
Pys&#228;kki
</th>
</tr>
</thead>
<tbody>
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['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 class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
+1
View File
@@ -0,0 +1 @@
<h1>testi2</h1>
+1
View File
@@ -0,0 +1 @@
<h1>testi3</h1>

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

@@ -1,53 +0,0 @@
#header {
height: 30%;
width: 100%;
background-color:#7c1330;
text-align: center;
}
#header-image {
display: block;
margin-left: auto;
margin-right: auto;
max-width: 100%;
}
.article-row {
min-height: 20vh;
margin: 10px 10px 10px 10px;
}
.article-thumb-col {
max-height: 200px;
text-align: left;
}
.article-title-col {
font-size: 3vw;
}
.thumbnail {
max-width: 355px;
max-height: 200px;
}
#sossoimage {
height: 300px;
position: relative;
left: 0px;
top: 0px;
}
.stretch {
width:100%;
height:100%;
}
#post {
height: 540px;
border:2px solid black;
}
#container {
max-height: 70%;
}
@@ -1,11 +0,0 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<link rel="stylesheet" href="/static/infoscreen/css/lunch.css">
<div ng-controller="LunchController">
<div id="container">
<div class="restaurant row" ng-repeat="restaurant in data">
<div class="lunch-option" ng-repeat="l in lunch">
<h3 ng-bind-html="l.title | unsafe"></h3>
</div>
</div>
</div>
</div>
@@ -1,10 +0,0 @@
<div ng-controller="infoadmin_lunchitem_create" style="margin-top:20px;">
<div>
Create new item to show restaurants. Name is used only as identifier
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -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",
"abbitem",
"sossoitem",
"lunchitem",
"eventitem",
"hslitem",
"websiteitem",
"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){
$scope.jobs = [];
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://old.sahkoinsinoorikilta.fi/api/news.php";
var url = "https://sahkoinsinoorikilta.fi/api/news.php";
var params = "?type=11&lang=fi&title_search=ABB&min_date="+min_date
$http.get(url+params).then(function(response){
$scope.jobs = _.filter(response.data, function(job){
@@ -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) {
$scope.items = [];
$http.get("/infoscreen/apyjson").then(function(response)
@@ -135,3 +112,35 @@ app.filter('unixTimeToDifference', function() {
return res;
}
})
app.controller('timetableCtrl',
function($scope, $http, $interval) {
function load() {
$http.get('/infoscreen/hsl_data')
.then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars
$scope.stoptimes = data.data;
$scope.error = data.data.error || null;
});
$http.get('/infoscreen/hsl_data/settings')
.then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars
$scope.departureThreshold = data.data['departure_threshold'];
$scope.hurryThreshold = data.data['hurry_threshold'];
});
}
function update_clock() {
$scope.clock = Date.now();
}
$scope.$on('$destroy', function() {
$interval.cancel(load_interval);
$interval.cancel(clock_interval);
});
var load_interval = $interval(load, 5000);
var clock_interval = $interval(update_clock, 1000);
update_clock();
load();
}
);
+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
in the response.
"""
resp = self.c.get("/infoscreen/items")
resp = self.c.get('/infoscreen/items')
content = resp.json()
self.assertTrue(len(content) > 0)
+29 -30
View File
@@ -1,7 +1,6 @@
"""File containing infoscreen urls."""
from django.urls import re_path
from django.conf import settings
from django.conf.urls import url
from infoscreen.views import index
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 createABBItem
from infoscreen.views import createSossoItem
from infoscreen.views import createLunchItem
from infoscreen.views import createHslItem
from infoscreen.views import createEventItem
from infoscreen.views import createExternalWebsiteItem
from infoscreen.views import create_rotation
from infoscreen.views import delete_rotation
from infoscreen.views import CurrentHSLView
from infoscreen.views import createApyItem
from infoscreen.views import hsl_timetable_settings
from infoscreen.views import get_apy_json
urlpatterns = [
re_path(r"^$", default),
re_path(r"^admin$", admin),
re_path(r"^(?P<idx>\d+)$", index),
re_path(r"^items$", info_items),
re_path(r"^rotation/(?P<idx>\d+)$", rotation),
re_path(r"^rotations$", rotations),
re_path(r"^instance$", createInstance),
re_path(r"^instance/(?P<idx>\d+)$", deleteInstance),
re_path(r"^types$", info_types),
re_path(r"^delete_item/(?P<type_id>\d+)/(?P<idx>\d+)$", delete_info_item),
re_path(r"^create_external_image$", createExternalImageInfoItem),
re_path(r"^create_image$", create_image_item),
re_path(r"^create_video$", create_video_item),
re_path(r"^create_abbitem$", createABBItem),
re_path(r"^create_sossoitem$", createSossoItem),
re_path(r"^create_lunchitem$", createLunchItem),
re_path(r"^create_eventitem$", createEventItem),
re_path(r"^create_apyitem$", createApyItem),
re_path(r"^create_websiteitem$", createExternalWebsiteItem),
re_path(r"^create_rotation$", create_rotation),
re_path(r"^delete_rotation/(?P<id>\d+)$", delete_rotation),
re_path(r"^apyjson", get_apy_json),
url(r'^$', default),
url(r'^admin$', admin),
url(r'^(?P<idx>\d+)$', index),
url(r'^items$', info_items),
url(r'^rotation/(?P<idx>\d+)$', rotation),
url(r'^rotations$', rotations),
url(r'^instance$', createInstance),
url(r'^instance/(?P<idx>\d+)$', deleteInstance),
url(r'^types$', info_types),
url(r'^delete_item/(?P<type_id>\d+)/(?P<idx>\d+)$', delete_info_item),
url(r'^create_external_image$', createExternalImageInfoItem),
url(r'^create_image$', create_image_item),
url(r'^create_video$', create_video_item),
url(r'^create_abbitem$', createABBItem),
url(r'^create_sossoitem$', createSossoItem),
url(r'^create_eventitem$', createEventItem),
url(r'^create_hslitem$', createHslItem),
url(r'^create_apyitem$', createApyItem),
url(r'^create_websiteitem$', createExternalWebsiteItem),
url(r'^create_rotation$', create_rotation),
url(r'^delete_rotation/(?P<id>\d+)$', delete_rotation),
url(r'^hsl_data$', CurrentHSLView),
url(r'^hsl_data/settings$', hsl_timetable_settings),
url(r'^apyjson', get_apy_json),
]
if settings.DEBUG:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()
+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.contrib.contenttypes.models import ContentType
from django.contrib.auth.decorators import permission_required, login_required
from django.db import DatabaseError
from infoscreen.models import UploadFileForm
import sikweb.settings as settings
@@ -15,28 +14,21 @@ import logging
import threading
import requests
from infoscreen.models import (
Rotation,
InfoItem,
InfoInstance,
ABBInfoItem,
ExternalImageInfoItem,
ImageInfoItem,
SossoInfoItem,
LunchItem,
EventInfoItem,
ExternalWebsiteInfoItem,
ImageUploadForm,
ApyInfoItem,
VideoInfoItem,
)
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem,
ImageInfoItem, SossoInfoItem, HslInfoItem)
from infoscreen.models import EventInfoItem
from infoscreen.models import ExternalWebsiteInfoItem
from infoscreen.models import ImageUploadForm
from infoscreen.models import ApyInfoItem
from infoscreen.models import VideoInfoItem
@login_required(login_url="/admin/login")
@permission_required("infoscreen.change_infoinstance", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.change_infoinstance', raise_exception=True)
def admin(request, *args, **kwargs):
"""Render infoscreen admin page."""
return render(request, "infoscreen/infoscreen_admin.html", {})
return render(request, 'infoscreen_admin.html', {})
def create_item_generator(model):
@@ -44,23 +36,20 @@ def create_item_generator(model):
@ensure_csrf_cookie
@require_http_methods(["POST"])
@login_required(login_url="/admin/login")
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_item(request, *args, **kwargs):
try:
data = json.loads(request.body.decode("utf-8"))
except json.JSONDecodeError:
except ValueError:
return HttpResponseBadRequest(
'{"status":"failure","error":"invalid json supplied"}'
)
'{"status":"failure","error":"invalid json supplied"}')
try:
model.create_from_dict(data)
return HttpResponse('{"status":"success"}')
except RuntimeError as e:
return HttpResponseBadRequest(
json.dumps({"status": "failure", "error": str(e)})
)
json.dumps({"status": "failure", "error": str(e)}))
return create_item
@@ -69,8 +58,8 @@ def delete_item_generator(model):
@ensure_csrf_cookie
@require_http_methods(["DELETE"])
@login_required(login_url="/admin/login")
@permission_required("infoscreen.delete_infoinstance", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
def delete_item(request, *args, **kwargs):
idx = kwargs.pop("idx", 0)
try:
@@ -82,18 +71,17 @@ def delete_item_generator(model):
try:
item.delete()
return HttpResponse('{"status":"success"}')
except DatabaseError:
except:
resp = HttpResponse('{"error" : "could not delete item"}')
resp.status_code = 500
return resp
return delete_item
# due to model structure this is little complicated
@ensure_csrf_cookie
@login_required(login_url="/admin/login")
@permission_required("infoscreen.delete_infoinstance", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
@require_http_methods(["DELETE"])
def delete_info_item(request, *args, **kwargs):
"""Delete info item."""
@@ -109,7 +97,7 @@ def delete_info_item(request, *args, **kwargs):
try:
item.delete()
return HttpResponse('{"status":"success"}')
except DatabaseError:
except:
resp = HttpResponse('{"error" : "could not delete item"}')
resp.status_code = 500
return resp
@@ -117,65 +105,64 @@ def delete_info_item(request, *args, **kwargs):
@require_http_methods(["POST"])
@ensure_csrf_cookie
@login_required(login_url="/admin/login")
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_image_item(request, *args, **kwargs):
"""Create image Infoscreen item."""
form = ImageUploadForm(request.POST, request.FILES)
if not form.is_valid():
return HttpResponseBadRequest(
'{"status": "failure",' '"error": "invalid data supplied"}'
)
return HttpResponseBadRequest('{"status": "failure",'
'"error": "invalid data supplied"}')
img = form.cleaned_data["image"]
name = form.cleaned_data["name"]
img = form.cleaned_data['image']
name = form.cleaned_data['name']
ImageInfoItem.objects.create(img=img, name=name)
return HttpResponse('{"status":"success"}')
@require_http_methods(["POST"])
@ensure_csrf_cookie
@login_required(login_url="/admin/login")
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_video_item(request, *args, **kwargs):
"""Create video Infoscreen item."""
form = UploadFileForm(request.POST, request.FILES)
if not form.is_valid():
return HttpResponseBadRequest(
'{"status": "failure",' '"error": "invalid data supplied"}'
)
return HttpResponseBadRequest('{"status": "failure",'
'"error": "invalid data supplied"}')
video = form.cleaned_data["video"]
name = form.cleaned_data["name"]
video = form.cleaned_data['video']
name = form.cleaned_data['name']
VideoInfoItem.objects.create(video=video, name=name)
return HttpResponse('{"status": "success"}')
@require_http_methods(["POST"])
@ensure_csrf_cookie
@login_required(login_url="/admin/login")
@permission_required("infoscreen.add_rotation", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.add_rotation', raise_exception=True)
def create_rotation(request, *args, **kwargs):
"""Create rotation."""
try:
data = json.loads(request.body.decode("utf-8"))
except json.JSONDecodeError:
except:
return HttpResponse('{"error": "bad post body!"}', status=400)
try:
name = data["name"]
Rotation.objects.create(name=name)
resp = HttpResponse(status=200)
except DatabaseError:
resp = HttpResponse('{"error" : "could not create rotation!"}', status=400)
except:
resp = HttpResponse(
'{"error" : "could not create rotation!"}', status=400)
return resp
@require_http_methods(["DELETE"])
@ensure_csrf_cookie
@login_required(login_url="/admin/login")
@permission_required("infoscreen.delete_rotation", raise_exception=True)
@login_required(login_url='/login')
@permission_required('infoscreen.delete_rotation', raise_exception=True)
def delete_rotation(request, *args, **kwargs):
"""Delete rotation."""
id = kwargs.pop("id", 0)
@@ -184,8 +171,9 @@ def delete_rotation(request, *args, **kwargs):
try:
Rotation.objects.filter(id=id).delete()
resp = HttpResponse(status=200)
except DatabaseError:
resp = HttpResponse('{"error" : "could not delete rotation!"}', status=400)
except:
resp = HttpResponse(
'{"error" : "could not delete rotation!"}', status=400)
return resp
@@ -194,7 +182,7 @@ createInstance = create_item_generator(InfoInstance)
deleteInstance = delete_item_generator(InfoInstance)
createABBItem = create_item_generator(ABBInfoItem)
createSossoItem = create_item_generator(SossoInfoItem)
createLunchItem = create_item_generator(LunchItem)
createHslItem = create_item_generator(HslInfoItem)
createExternalImageInfoItem = create_item_generator(ExternalImageInfoItem)
createExternalWebsiteItem = create_item_generator(ExternalWebsiteInfoItem)
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.views.decorators.http import require_http_methods
from django.conf import settings
from django.db import DatabaseError
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.hsl_fetcher import fetch as hsl_fetch
import json
import logging
import threading
import requests
@require_http_methods(["GET"])
def index(request, idx, *args, **kwargs):
"""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"])
@@ -23,7 +22,7 @@ def default(request, *args, **kwargs):
"""Try getting first rotation item."""
try:
first = Rotation.objects.all()[0].id
except DatabaseError:
except:
first = 0
return index(request, first, *args, **kwargs)
@@ -32,8 +31,7 @@ def default(request, *args, **kwargs):
def get_apy_json(request):
"""Render APY diilikone page."""
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"])
@@ -62,12 +60,10 @@ def info_types(request, *args, **kwargs):
types = []
classes = InfoItem.get_subclasses()
for c in classes:
types.append(
{
"name": c.display_name,
"create_template_url": c.get_create_template_url(),
}
)
types.append({
"name": c.display_name,
"create_template_url": c.get_create_template_url(),
})
return HttpResponse(json.dumps(types))
@@ -81,3 +77,25 @@ def info_items(request, *args, **kwargs):
items.append(i.get_dict())
return JsonResponse(items, safe=False)
@require_http_methods(["GET"])
def hsl_timetable_settings(request, *args, **kwargs):
"""Set HSL timetable settings."""
d = {"departure_threshold": settings.HSL_DEPARTURE_THRESHOLD,
"hurry_threshold": settings.HSL_HURRY_THRESHOLD}
return JsonResponse(d, status=200)
@require_http_methods(["GET"])
def CurrentHSLView(request, *args, **kwargs):
"""Get HSL data and return it."""
try:
api_resp = hsl_fetch()
except Exception as ex:
logging.exception('Failed to fetch HSL timetables.')
error = {'error': 'Aikataulujen haku epäonnistui.'}
return JsonResponse(error, status=200)
return JsonResponse(api_resp, status=200, safe=False)
-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