Compare commits
316 Commits
feature-nobot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8164094b44 | |||
| fd12f35e2e | |||
| f69615437a | |||
| 533f367b4e | |||
| 93202d8a11 | |||
| e1e1adc397 | |||
| 0df2d4ab46 | |||
| 4e0a93631d | |||
| be9e308587 | |||
| 738051355b | |||
| 8550a9a02b | |||
| 1cf67c7686 | |||
| 703bb91bfd | |||
| 2ff0cab544 | |||
| 6b4a00ebd8 | |||
| 677c1400fa | |||
| a2e08d0ea6 | |||
| 901f2bed96 | |||
| 6004156b6f | |||
| e00323bffe | |||
| 6ef0dbf91b | |||
| 2169bad90d | |||
| 63a4781574 | |||
| e735ebe64a | |||
| f9db8476a1 | |||
| 2a24544056 | |||
| fb7bee5480 | |||
| 8355d10635 | |||
| 21892e277e | |||
| 81e1f994eb | |||
| c79d824a8a | |||
| fc6e02b71b | |||
| 1e14f98f9d | |||
| 8815ccf667 | |||
| b694370572 | |||
| f51611bbee | |||
| fda9acfb2f | |||
| 30c6e4809b | |||
| 4373f37dfe | |||
| 39754a5e63 | |||
| 0be3ee69be | |||
| c6f0f4615b | |||
| 3ac5400b79 | |||
| 429d3a0602 | |||
| 1b086843dc | |||
| b9280ea026 | |||
| b36022e546 | |||
| 9bb57840a7 | |||
| 36bd74c6cc | |||
| a2615ae27d | |||
| f7de9e32d3 | |||
| 57d8c4321f | |||
| 632eedea9c | |||
| 1405d89d9a | |||
| da1ae8d721 | |||
| 52a83b9336 | |||
| f67ce55d60 | |||
| d8fdc2cc74 | |||
| 5e1390ab6b | |||
| eb2ae3368a | |||
| 99348dc297 | |||
| 715b309c89 | |||
| f7f63b8670 | |||
| 70676d5203 | |||
| 037e4ae6e8 | |||
| 53742e5caa | |||
| 8d6f13b61d | |||
| 03982ee620 | |||
| 2affae7bfd | |||
| a8923b63d6 | |||
| 19975877cb | |||
| 2e0fad4bb2 | |||
| f0179c1840 | |||
| 37a9750d4d | |||
| 5575186570 | |||
| 9e179d5e06 | |||
| f7659e1e1b | |||
| ea9a732803 | |||
| a4ae136e1a | |||
| 0026b788b2 | |||
| 570fff1d7d | |||
| da3a484f6c | |||
| 9d116528b9 | |||
| a310d51f5e | |||
| 6732e30213 | |||
| afbdca1501 | |||
| 4e59eee200 | |||
| 03e4ccdf5d | |||
| bb0b2a2628 | |||
| 16454ebdf6 | |||
| 32d636d3ee | |||
| 2e2464fb5f | |||
| c91b99cdb1 | |||
| caf2113e49 | |||
| c1a1f6e534 | |||
| f79d1467f7 | |||
| 40cf9121b6 | |||
| ca73eba609 | |||
| fe46d57108 | |||
| 70e1835a4f | |||
| b7f17671d9 | |||
| 34659403a8 | |||
| 4fbf5fe0a4 | |||
| c6be0e6562 | |||
| 3623c7e9f4 | |||
| 298db5b78e | |||
| 1ca6de3090 | |||
| 07d0f2aa47 | |||
| 93e122b8a8 | |||
| 9678b663a0 | |||
| 992a2ec8e0 | |||
| b41bd41a54 | |||
| 30f59c36fb | |||
| f51d71e045 | |||
| 9c66238b82 | |||
| 2f0143a9ae | |||
| 45ff2c3757 | |||
| 321d45b628 | |||
| 2628d753f5 | |||
| a603e2dff8 | |||
| 96e05d908d | |||
| a5bf5668eb | |||
| 1ec5082faf | |||
| 6da5b97e19 | |||
| 7fd30e3eba | |||
| 3e9084ca1d | |||
| a28f82d31e | |||
| 04ecb8fc7e | |||
| 3e707e58a5 | |||
| 70d7f55996 | |||
| e408809e58 | |||
| 33fd4012f1 | |||
| 228938b695 | |||
| 72e91e3d62 | |||
| 3f6a719e9d | |||
| 490b99a848 | |||
| 0a899f5600 | |||
| 7825cc7293 | |||
| 8bb6e9e9a7 | |||
| 53c3acd39f | |||
| c45bcc5442 | |||
| dd0254a08e | |||
| 9b53fb4bc0 | |||
| 2383e2089d | |||
| e17c3ad92c | |||
| 362d981532 | |||
| e12be3c2f6 | |||
| 3edae7f967 | |||
| 4d159b2793 | |||
| cb3b831f7a | |||
| acba330694 | |||
| eb22368055 | |||
| 5f467323b5 | |||
| fac2f9b367 | |||
| 7319c32d73 | |||
| b3a484ce55 | |||
| 337b774074 | |||
| e70e598c57 | |||
| 5eef2f685c | |||
| d1953ef24c | |||
| a9c122c0d4 | |||
| ec4317d9e7 | |||
| d4a219290b | |||
| cff84816fc | |||
| 7881a24eb1 | |||
| e74580fdde | |||
| a0765ca18b | |||
| 913eb1cedf | |||
| 7035ebccca | |||
| 2031146fc7 | |||
| 35f30300b3 | |||
| 342f2862a5 | |||
| 3536ca5922 | |||
| 50ab7bc1f9 | |||
| 102d8f82d6 | |||
| f302c0a17d | |||
| a2b7086e9a | |||
| 704652c643 | |||
| 8741f6b113 | |||
| 9ffb79aa52 | |||
| a2551cc110 | |||
| 8c90471eb1 | |||
| 8d7bd7067e | |||
| db3e3ae291 | |||
| 1afd476c18 | |||
| 322cc9d6fb | |||
| 5f9a7a6994 | |||
| 3809ba9726 | |||
| 79cc0bcd55 | |||
| 7cb03d40d4 | |||
| 34faf53347 | |||
| 8d2a26a9d1 | |||
| a05edb73d3 | |||
| 2c69e1b945 | |||
| 9cae6ea890 | |||
| 49a3b1449e | |||
| 02634c8e02 | |||
| fc73424665 | |||
| 11efcdd579 | |||
| a0f062c697 | |||
| 1dc5d45e96 | |||
| 7382c4e4bf | |||
| 867996ae27 | |||
| 798c860091 | |||
| 86a65e4680 | |||
| 4a530826a8 | |||
| 6f316401f7 | |||
| 8a8820be2f | |||
| 51278fd8bc | |||
| 2d86d548a3 | |||
| b610a8af6e | |||
| 6380d39afb | |||
| 6022b11dc1 | |||
| 9420b1fd05 | |||
| fcd23c07ab | |||
| c92193057c | |||
| a845be5394 | |||
| c4a0f5a0ea | |||
| 14735d8898 | |||
| d8250c691c | |||
| 87d14240cf | |||
| 50d4006b96 | |||
| d9b006904a | |||
| 3aa225f3cd | |||
| a4e1aa5032 | |||
| 72ea31a887 | |||
| 912ce44513 | |||
| 6d093af511 | |||
| be7a5fe2e2 | |||
| 40f6558a6e | |||
| 808c27e104 | |||
| b8342ba66a | |||
| 9d05cd8290 | |||
| 4860c0a0d9 | |||
| 40b824355b | |||
| 9d4c041153 | |||
| ce72ff7801 | |||
| 5f69f34bf3 | |||
| fb3328cb23 | |||
| c0db047cd9 | |||
| e2c32e81a7 | |||
| dac0fa1953 | |||
| 63672328f0 | |||
| a927ab8a13 | |||
| c6374d88b6 | |||
| f72017df01 | |||
| 26635bbbcc | |||
| 86973562ff | |||
| 0c1bdba358 | |||
| 8481e963a3 | |||
| 39ba51f11a | |||
| d889df951c | |||
| 19f8ff006c | |||
| ed77526f5c | |||
| 6f6dc09f01 | |||
| 5049ef415d | |||
| 441227bc15 | |||
| 78575075e5 | |||
| 4029031bf0 | |||
| f1ea08bc30 | |||
| e68f04117d | |||
| d07914bdb5 | |||
| 38eb4cd9cd | |||
| ab104d2acd | |||
| 0376acdc9e | |||
| 6a65ca32d7 | |||
| 99739cd035 | |||
| 8454e67a92 | |||
| 33c7c20140 | |||
| 5a77b1546d | |||
| 81971b6da4 | |||
| a68fa9a6d6 | |||
| 034e04a788 | |||
| d416fda39e | |||
| e4fbb58026 | |||
| 56dfd57698 | |||
| 8632aa01da | |||
| 8b1f668d38 | |||
| 027bf5e7bd | |||
| be22fea3f2 | |||
| 5e434408b0 | |||
| 41762e920f | |||
| f390e1fb1f | |||
| 4e35a73a4b | |||
| 6955659acb | |||
| dbc7811651 | |||
| 6c4a26eb44 | |||
| fb9dbe2cd2 | |||
| b346c2122a | |||
| c9c814e405 | |||
| 0f72a7ea21 | |||
| 135c0309b5 | |||
| bd4551a222 | |||
| 7fee47f150 | |||
| b0dd95fa86 | |||
| 9516dbbbea | |||
| 5b2546217c | |||
| 2646914b8a | |||
| 17c00e519b | |||
| 389771d106 | |||
| ed4e226186 | |||
| bc7a8ebd17 | |||
| cfdac40d3b | |||
| ec4be22552 | |||
| 06d5e3c6f4 | |||
| 89cd91dbad | |||
| 44c091f2d5 | |||
| c1b3bedf8d | |||
| 3c16029e88 | |||
| 41d6f17716 | |||
| 2649afdd4b | |||
| 8ee514326a | |||
| 48e34ece4f | |||
| d2af34bf0c | |||
| ddf4ad7b8d | |||
| 3169191a0d |
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Deploying to development."
|
||||
set -e
|
||||
set -x
|
||||
|
||||
pushd deployment
|
||||
|
||||
docker-compose down
|
||||
docker pull "$1"
|
||||
docker-compose up -d
|
||||
|
||||
popd
|
||||
|
||||
set +x
|
||||
set +e
|
||||
@@ -0,0 +1,32 @@
|
||||
.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
|
||||
@@ -1,10 +1,13 @@
|
||||
HOST=api.dev.sahkoinsinoorikilta.fi
|
||||
DEPLOY_ENV=local
|
||||
SENTRY_DSN=
|
||||
HOST=localhost
|
||||
DEBUG=True
|
||||
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
|
||||
TG_BOT_TOKEN=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PASSWD=
|
||||
DB_NAME=postgres
|
||||
DB_USER=postgres
|
||||
DB_PASSWD=postgres
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
EMAIL_API_KEY=
|
||||
GROUP_KEY=
|
||||
GOOGLE_CREDS='{}'
|
||||
|
||||
+7
-4
@@ -1,10 +1,13 @@
|
||||
DEPLOY_ENV=local
|
||||
#SENTRY_DSN=
|
||||
HOST=localhost
|
||||
DEBUG=True
|
||||
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
|
||||
TG_BOT_TOKEN=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PASSWD=
|
||||
DB_NAME=postgres
|
||||
DB_USER=postgres
|
||||
DB_PASSWD=postgres
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_PORT=5432
|
||||
EMAIL_API_KEY=
|
||||
GROUP_KEY=
|
||||
GOOGLE_CREDS='{}'
|
||||
|
||||
+7
-16
@@ -1,23 +1,14 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.env
|
||||
*~
|
||||
*.pyc
|
||||
*.sqlite3
|
||||
uwsgi.ini
|
||||
uwsgi.log
|
||||
members/logs/*
|
||||
.idea/
|
||||
logs/
|
||||
/collected_static/
|
||||
/media/
|
||||
logs/
|
||||
members/logs/*
|
||||
node_modules/
|
||||
.coverage
|
||||
db.sqlite3
|
||||
requirements_henu.txt
|
||||
/collected_static/
|
||||
mydatabase
|
||||
settings.json
|
||||
.vscode/
|
||||
.DS_Store
|
||||
.idea/
|
||||
*.code-workspace
|
||||
sik_test
|
||||
venv/
|
||||
venv/
|
||||
.venv/
|
||||
+86
-12
@@ -1,13 +1,17 @@
|
||||
stages:
|
||||
- setup
|
||||
- audit
|
||||
- lint
|
||||
- test
|
||||
- publish
|
||||
- deploy
|
||||
- cleanup
|
||||
|
||||
install:
|
||||
image: node:14
|
||||
stage: setup
|
||||
only:
|
||||
- pushes
|
||||
script:
|
||||
- npm ci
|
||||
artifacts:
|
||||
@@ -15,9 +19,27 @@ install:
|
||||
- node_modules
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: python:3.9
|
||||
stage: audit
|
||||
only:
|
||||
- pushes
|
||||
- develop
|
||||
except:
|
||||
- master
|
||||
needs: []
|
||||
before_script:
|
||||
- pip install poetry==1.3.1
|
||||
- poetry config virtualenvs.create false
|
||||
- poetry install --no-interaction --no-ansi
|
||||
script:
|
||||
- safety check
|
||||
|
||||
test:
|
||||
image: python:3.7
|
||||
image: python:3.9
|
||||
stage: test
|
||||
only:
|
||||
- pushes
|
||||
needs: []
|
||||
services:
|
||||
- postgres:12
|
||||
@@ -27,24 +49,30 @@ test:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
|
||||
DB_HOST: postgres
|
||||
before_script:
|
||||
- pip install poetry==1.3.1
|
||||
- poetry config virtualenvs.create false
|
||||
- poetry install --no-interaction --no-ansi
|
||||
script:
|
||||
- python -V
|
||||
- pip install -r requirements.txt
|
||||
- python manage.py migrate --noinput
|
||||
- python manage.py createdefaultadmin
|
||||
- python manage.py test
|
||||
|
||||
lint:py:
|
||||
image: python:3.7
|
||||
image: python:3.9
|
||||
stage: lint
|
||||
only:
|
||||
- pushes
|
||||
needs: []
|
||||
script:
|
||||
- pip install pycodestyle
|
||||
- pycodestyle --config=pycodestyle.cfg --count .
|
||||
- pip install black==22.3.0
|
||||
- black --check .
|
||||
|
||||
lint:js:
|
||||
image: node:14
|
||||
stage: lint
|
||||
only:
|
||||
- pushes
|
||||
needs: ["install"]
|
||||
script:
|
||||
- npm run lint:js
|
||||
@@ -52,13 +80,15 @@ lint:js:
|
||||
lint:md:
|
||||
image: node:14
|
||||
stage: lint
|
||||
only:
|
||||
- pushes
|
||||
needs: ["install"]
|
||||
script:
|
||||
- npm run lint:md
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
image: docker:stable
|
||||
stage: publish
|
||||
needs: ["test", "lint:py", "lint:js", "lint:md"]
|
||||
services:
|
||||
- docker:stable-dind
|
||||
@@ -72,8 +102,8 @@ publish:
|
||||
- docker push "$IMAGE_NAME"
|
||||
|
||||
deploy:dev:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
stage: deploy
|
||||
only:
|
||||
- develop
|
||||
environment:
|
||||
@@ -87,7 +117,7 @@ deploy:dev:
|
||||
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
- 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:
|
||||
@@ -100,7 +130,7 @@ deploy:production:
|
||||
- master
|
||||
environment:
|
||||
name: production
|
||||
url: api.sahkoinsinoorikilta.fi
|
||||
url: https://api.sahkoinsinoorikilta.fi
|
||||
when: manual
|
||||
variables:
|
||||
DOCKER_HOST: $CI_DOCKER_HOST
|
||||
@@ -110,8 +140,52 @@ deploy:production:
|
||||
- echo "$TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
- 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 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"
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
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
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
PURPLE='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
source "${VIRTUAL_ENV}/bin/activate"
|
||||
. "${VIRTUAL_ENV}/bin/activate"
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.7.4
|
||||
3.9
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
global_static
|
||||
+22
-14
@@ -1,20 +1,28 @@
|
||||
FROM python:3.7-alpine
|
||||
FROM python:3.9-slim-buster as builder
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
WORKDIR /app
|
||||
COPY requirements.txt ./
|
||||
COPY requirements.production.txt ./
|
||||
COPY . ./
|
||||
|
||||
# uWSGI, gunicorn etc.
|
||||
RUN apk add --no-cache python3-dev build-base linux-headers pcre-dev openssl bash \
|
||||
# PSQL
|
||||
&& apk add --no-cache postgresql-dev \
|
||||
# Pillow
|
||||
&& apk add --no-cache jpeg-dev zlib-dev \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install -r requirements.txt \
|
||||
&& pip install -r requirements.production.txt
|
||||
ENV POETRY_VERSION=1.3.1
|
||||
|
||||
RUN pip install "poetry==$POETRY_VERSION"
|
||||
RUN poetry export --without-hashes > requirements.txt
|
||||
|
||||
FROM python:3.9-slim-buster as server
|
||||
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
COPY --from=builder requirements.txt ./
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
# prevents python creating .pyc files
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
# pip
|
||||
PIP_NO_CACHE_DIR=off \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y build-essential
|
||||
RUN pip install --no-deps -r requirements.txt
|
||||
|
||||
RUN python manage.py collectstatic --noinput
|
||||
|
||||
CMD ["sh", "-c", "./production_entrypoint.sh"]
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# Web 2.0 Backend
|
||||
|
||||
[Django](https://www.djangoproject.com/) backend containing multiple small applications and api for Next.js frontend.
|
||||
|
||||
* **Web app:** Backend for the main website.
|
||||
* **Member register:** Data table app for viewing and modifying the member register, member applications and membership payments.
|
||||
* **Kaehmy:** Form for creating and listing kaehmys
|
||||
* **Ohlhafv:** Form for creating and listing ohlhafv challenges.
|
||||
* **Infoscreen:** Angular-based slideshow app for the guild room's screens.
|
||||
## Installation
|
||||
|
||||
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the development branch:
|
||||
|
||||
```bash
|
||||
git clone git@gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-backend.git
|
||||
cd web2.0-backend
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
Copy env file for local use:
|
||||
```bash
|
||||
cp .env.dev .env
|
||||
```
|
||||
|
||||
### Poetry
|
||||
|
||||
For depedencies and virtual environment, we use [poetry](https://python-poetry.org/).
|
||||
|
||||
First install [python](https://wiki.python.org/moin/BeginnersGuide/Download). Then install poetry:
|
||||
|
||||
```bash
|
||||
python3 -m pip install poetry==1.3.1
|
||||
```
|
||||
|
||||
The easiest integration with VSCode is to have poetry install virtual environment in project folder, configured with CMD
|
||||
|
||||
```bash
|
||||
python3 -m poetry config virtualenvs.in-project true
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
We use Node.js for few development tasks, like linting. Easiest way to install Node is [nvm](https://github.com/nvm-sh/nvm). After installing install dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
TODO: List scripts
|
||||
|
||||
### Database
|
||||
|
||||
To run a local development database **[docker](https://docs.docker.com/engine/install/)** is recommended. If you want to additianally use a db management tool **[pgAdmin](https://www.pgadmin.org/download/)** is nice.
|
||||
|
||||
After installing docker use the following to create a database:
|
||||
```bash
|
||||
docker run --name sik.web.db -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:12
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Activate virtual environment in shell
|
||||
|
||||
```bash
|
||||
python3 -m poetry shell
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
### Initializing data
|
||||
|
||||
Run the following `manage.py` commands to initialize a new database. Do not run these in production without thinking!
|
||||
|
||||
```bash
|
||||
python manage.py migrate # run migrations
|
||||
python manage.py createdefaultadmin # creates an admin user
|
||||
python manage.py initialize # creates user groups
|
||||
python manage.py createdummydata # creates dummy members to the member register
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Visit [https://localhost:8000](https://localhost:8000) in your browser!
|
||||
|
||||
Using address `0.0.0.0` will bind to all IP addresses. Using `localhost` will only bind to your machine.
|
||||
|
||||
```bash
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
### Development workflow
|
||||
|
||||
When you start working on a feature, create a feature branch for your changes. These feature branches should be prefixed with `feature`.
|
||||
|
||||
Example of creating a feature branch:
|
||||
|
||||
```bash
|
||||
git checkout -b feature-branch-name
|
||||
```
|
||||
|
||||
When your changes are ready and the code works without errors, submit a merge request to `develop` in GitLab. Another developer reviews your changes and runs the merge. Feature branches should be closed on merge.
|
||||
|
||||
Bugfixes do not need their own feature branches and can be pushed straight to `develop`, but if the fix needs a notable amount of work, it should be done in a `bugfix` branch instead.
|
||||
|
||||
Merge requests to `master` should be reviewed by multiple developers. Only a moderator can accept merge requests to `master`.
|
||||
|
||||
### Linting
|
||||
|
||||
Lint python files using `black` with
|
||||
|
||||
```bash
|
||||
npm run lint:py # check changes
|
||||
npm run lint:py:fix # fix errors
|
||||
```
|
||||
|
||||
Lint javascript and markdown using `eslint` and `remark` with
|
||||
|
||||
```bash
|
||||
npm run lint:md # markdown
|
||||
npm run lint:js # javascript
|
||||
```
|
||||
|
||||
Use an editor with linting capabilities to write pretty code that passes linting. Examples include _VSCode_, _Atom_ and _Pycharm_.
|
||||
|
||||
### Unit tests
|
||||
|
||||
Run unit tests with
|
||||
|
||||
```bash
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
Due to the mostly static nature of the project, most elements are difficult to properly unit test. If you write code with actual logic, make sure to write at least one unit or integration test that tests your code's core functionality.
|
||||
|
||||
Tests are located in `tests.py` under every subproject.
|
||||
|
||||
## Production
|
||||
|
||||
Project is run in production with Docker. See `Dockerfile` for details.
|
||||
|
||||
For more information about deployment check **[infra](https://gitlab.com/sahkoinsinoorikilta/vtmk/infra)** repository.
|
||||
|
||||
## GitLab CI
|
||||
|
||||
All pushed changes go through the GitLab Continuous Integration, which consists of automated unit testing and linting. Make sure your changes pass both before merging to `develop` or `master`.
|
||||
@@ -13,7 +13,6 @@ services:
|
||||
web:
|
||||
build: .
|
||||
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend
|
||||
command: ["bash", "-c", "cd /app & bash setup.sh --no-input --no-npm && gunicorn -w 4 -b 0.0.0.0:8000 sikweb.wsgi"]
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
|
||||
+9
-2
@@ -2,8 +2,15 @@
|
||||
|
||||
from django.contrib import admin
|
||||
from infoscreen.models import (
|
||||
Rotation, InfoItem, InfoInstance, ImageInfoItem,
|
||||
ExternalImageInfoItem, ABBInfoItem, ExternalWebsiteInfoItem, VideoInfoItem)
|
||||
Rotation,
|
||||
InfoItem,
|
||||
InfoInstance,
|
||||
ImageInfoItem,
|
||||
ExternalImageInfoItem,
|
||||
ABBInfoItem,
|
||||
ExternalWebsiteInfoItem,
|
||||
VideoInfoItem,
|
||||
)
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(Rotation)
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ from django.apps import AppConfig
|
||||
class InfoscreenConfig(AppConfig):
|
||||
"""Infoscreen app configuration."""
|
||||
|
||||
name = 'infoscreen'
|
||||
name = "infoscreen"
|
||||
|
||||
@@ -11,81 +11,173 @@ 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,15 +9,25 @@ 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,33 +9,63 @@ 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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,16 +9,26 @@ 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,14 @@ 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',
|
||||
name="HSLDataModel",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='HslInfoItem',
|
||||
name="HslInfoItem",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -7,15 +7,25 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('infoscreen', '0006_delete_hsldatamodel'),
|
||||
("infoscreen", "0006_delete_hsldatamodel"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LunchItem',
|
||||
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')),
|
||||
(
|
||||
"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",),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
+52
-54
@@ -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 ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class InfoItem(models.Model):
|
||||
@@ -16,6 +16,7 @@ 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)
|
||||
@@ -23,14 +24,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):
|
||||
@@ -42,14 +43,13 @@ 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
|
||||
@@ -139,7 +139,7 @@ class ExternalWebsiteInfoItem(InfoItem):
|
||||
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,23 +152,22 @@ 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:
|
||||
@@ -241,14 +240,14 @@ class ImageInfoItem(InfoItem):
|
||||
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):
|
||||
@@ -263,7 +262,7 @@ class VideoInfoItem(InfoItem):
|
||||
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
|
||||
|
||||
|
||||
@@ -285,7 +284,7 @@ class ExternalImageInfoItem(InfoItem):
|
||||
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
|
||||
@@ -298,15 +297,14 @@ 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:
|
||||
@@ -319,12 +317,15 @@ class ExternalImageInfoItem(InfoItem):
|
||||
class InfoInstance(models.Model):
|
||||
"""Class for Info instance in Infoscreen."""
|
||||
|
||||
rotation = models.ForeignKey('Rotation', related_name='instances', on_delete=models.CASCADE)
|
||||
id = models.AutoField(primary_key=True)
|
||||
rotation = models.ForeignKey(
|
||||
"Rotation", related_name="instances", on_delete=models.CASCADE
|
||||
)
|
||||
duration = models.FloatField(default=15.0) # seconds
|
||||
# generic relation to some kind of InfoItem
|
||||
item_id = models.PositiveIntegerField()
|
||||
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):
|
||||
@@ -337,31 +338,27 @@ 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):
|
||||
@@ -370,21 +367,20 @@ 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):
|
||||
@@ -395,6 +391,7 @@ 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()
|
||||
|
||||
@@ -402,5 +399,6 @@ 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()
|
||||
|
||||
@@ -3,7 +3,7 @@ body {
|
||||
}
|
||||
|
||||
#header:after {
|
||||
content: " ";
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
max-height 200px;
|
||||
}
|
||||
|
||||
#sossoimage {
|
||||
#sossoimage {
|
||||
height:300px;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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>
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+24
-23
@@ -1,6 +1,6 @@
|
||||
"""File containing infoscreen urls."""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
from django.conf import settings
|
||||
|
||||
from infoscreen.views import index
|
||||
@@ -27,30 +27,31 @@ from infoscreen.views import createApyItem
|
||||
from infoscreen.views import get_apy_json
|
||||
|
||||
urlpatterns = [
|
||||
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_lunchitem$', createLunchItem),
|
||||
url(r'^create_eventitem$', createEventItem),
|
||||
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'^apyjson', get_apy_json),
|
||||
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),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
@@ -16,16 +16,27 @@ import threading
|
||||
import requests
|
||||
|
||||
from infoscreen.models import (
|
||||
Rotation, InfoItem, InfoInstance, ABBInfoItem, ExternalImageInfoItem,
|
||||
ImageInfoItem, SossoInfoItem, LunchItem, EventInfoItem,
|
||||
ExternalWebsiteInfoItem, ImageUploadForm, ApyInfoItem, VideoInfoItem)
|
||||
Rotation,
|
||||
InfoItem,
|
||||
InfoInstance,
|
||||
ABBInfoItem,
|
||||
ExternalImageInfoItem,
|
||||
ImageInfoItem,
|
||||
SossoInfoItem,
|
||||
LunchItem,
|
||||
EventInfoItem,
|
||||
ExternalWebsiteInfoItem,
|
||||
ImageUploadForm,
|
||||
ApyInfoItem,
|
||||
VideoInfoItem,
|
||||
)
|
||||
|
||||
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.change_infoinstance', raise_exception=True)
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.change_infoinstance", raise_exception=True)
|
||||
def admin(request, *args, **kwargs):
|
||||
"""Render infoscreen admin page."""
|
||||
return render(request, 'infoscreen:infoscreen_admin.html', {})
|
||||
return render(request, "infoscreen/infoscreen_admin.html", {})
|
||||
|
||||
|
||||
def create_item_generator(model):
|
||||
@@ -33,20 +44,23 @@ 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="/admin/login")
|
||||
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
|
||||
def create_item(request, *args, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
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
|
||||
|
||||
|
||||
@@ -55,8 +69,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="/admin/login")
|
||||
@permission_required("infoscreen.delete_infoinstance", raise_exception=True)
|
||||
def delete_item(request, *args, **kwargs):
|
||||
idx = kwargs.pop("idx", 0)
|
||||
try:
|
||||
@@ -72,13 +86,14 @@ def delete_item_generator(model):
|
||||
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="/admin/login")
|
||||
@permission_required("infoscreen.delete_infoinstance", raise_exception=True)
|
||||
@require_http_methods(["DELETE"])
|
||||
def delete_info_item(request, *args, **kwargs):
|
||||
"""Delete info item."""
|
||||
@@ -102,42 +117,44 @@ 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="/admin/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="/admin/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="/admin/login")
|
||||
@permission_required("infoscreen.add_rotation", raise_exception=True)
|
||||
def create_rotation(request, *args, **kwargs):
|
||||
"""Create rotation."""
|
||||
try:
|
||||
@@ -150,16 +167,15 @@ def create_rotation(request, *args, **kwargs):
|
||||
Rotation.objects.create(name=name)
|
||||
resp = HttpResponse(status=200)
|
||||
except DatabaseError:
|
||||
resp = HttpResponse(
|
||||
'{"error" : "could not create rotation!"}', status=400)
|
||||
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="/admin/login")
|
||||
@permission_required("infoscreen.delete_rotation", raise_exception=True)
|
||||
def delete_rotation(request, *args, **kwargs):
|
||||
"""Delete rotation."""
|
||||
id = kwargs.pop("id", 0)
|
||||
@@ -169,8 +185,7 @@ def delete_rotation(request, *args, **kwargs):
|
||||
Rotation.objects.filter(id=id).delete()
|
||||
resp = HttpResponse(status=200)
|
||||
except DatabaseError:
|
||||
resp = HttpResponse(
|
||||
'{"error" : "could not delete rotation!"}', status=400)
|
||||
resp = HttpResponse('{"error" : "could not delete rotation!"}', status=400)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import requests
|
||||
@require_http_methods(["GET"])
|
||||
def index(request, idx, *args, **kwargs):
|
||||
"""Render infoscreen index page."""
|
||||
return render(request, 'infoscreen_index.html', {'rotation': idx})
|
||||
return render(request, "infoscreen/infoscreen_index.html", {"rotation": idx})
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -32,7 +32,8 @@ 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"])
|
||||
@@ -61,10 +62,12 @@ 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))
|
||||
|
||||
|
||||
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
from django.contrib import admin
|
||||
from modeltranslation.admin import TranslationAdmin
|
||||
|
||||
from kaehmy.models import Application, Comment, CustomRole, PresetRole, TelegramChannel
|
||||
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)
|
||||
admin.site.register(TelegramChannel)
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class KaehmyConfig(AppConfig):
|
||||
name = 'kaehmy'
|
||||
name = "kaehmy"
|
||||
|
||||
+55
-30
@@ -1,17 +1,21 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from kaehmy.models import PresetRole, CustomRole, Application, Comment, KaehmyBaseRole
|
||||
from kaehmy.models import PresetRole, CustomRole, Application, Comment, BaseRole
|
||||
|
||||
|
||||
class CheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
|
||||
option_template_name = 'checkbox_option.html'
|
||||
option_template_name = "checkbox_option.html"
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
dic = super(CheckboxSelectMultiple, self).create_option(name, value, label, selected, index, subindex, attrs)
|
||||
description = PresetRole.objects.get(id=value).description
|
||||
dic['description'] = description
|
||||
def create_option(
|
||||
self, name, formIterator, label, selected, index, subindex=None, attrs=None
|
||||
):
|
||||
dic = super(CheckboxSelectMultiple, self).create_option(
|
||||
name, formIterator, label, selected, index, subindex, attrs
|
||||
)
|
||||
description = PresetRole.objects.get(id=formIterator.value).description
|
||||
dic["description"] = description
|
||||
return dic
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -25,30 +29,46 @@ class ApplicationForm(forms.ModelForm):
|
||||
"""Meta for class Application."""
|
||||
|
||||
model = Application
|
||||
fields = ['name', 'email', 'phone_number', 'year',
|
||||
'preset_roles', 'custom_roles', 'custom_role_name',
|
||||
'custom_role_is_board', 'text']
|
||||
fields = [
|
||||
"name",
|
||||
"email",
|
||||
"phone_number",
|
||||
"year",
|
||||
"preset_roles",
|
||||
"custom_roles",
|
||||
"custom_role_name",
|
||||
"custom_role_is_board",
|
||||
"text",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ApplicationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["email"].label = _('Email (not public)')
|
||||
self.fields["phone_number"].label = _('Phone number (not public)')
|
||||
self.fields["email"].label = _("Email (not public)")
|
||||
self.fields["phone_number"].label = _("Phone number (not public)")
|
||||
|
||||
custom_roles_exist = CustomRole.objects.all().exists()
|
||||
self.fields["custom_roles"].widget = forms.widgets.CheckboxSelectMultiple() if custom_roles_exist else forms.HiddenInput()
|
||||
self.fields["custom_roles"].widget = (
|
||||
forms.widgets.CheckboxSelectMultiple()
|
||||
if custom_roles_exist
|
||||
else forms.HiddenInput()
|
||||
)
|
||||
self.fields["custom_roles"].help_text = ""
|
||||
self.fields["custom_roles"].label = _('Custom roles')
|
||||
self.fields["custom_roles"].label = _("Custom roles")
|
||||
self.fields["custom_roles"].queryset = CustomRole.objects.all()
|
||||
|
||||
for cat_id, category in KaehmyBaseRole.CATEGORIES:
|
||||
key = 'preset_roles_{}'.format(cat_id)
|
||||
qset = PresetRole.objects.filter(category=cat_id).order_by('category', '-is_board')
|
||||
for cat_id, category in BaseRole.CATEGORIES:
|
||||
key = "preset_roles_{}".format(cat_id)
|
||||
qset = PresetRole.objects.filter(category=cat_id).order_by(
|
||||
"category", "-is_board"
|
||||
)
|
||||
self.fields[key] = forms.ModelMultipleChoiceField(qset)
|
||||
self.fields[key].widget = CheckboxSelectMultiple(attrs={
|
||||
'title': _('Preset roles'),
|
||||
'name': 'preset_roles',
|
||||
})
|
||||
self.fields[key].widget = CheckboxSelectMultiple(
|
||||
attrs={
|
||||
"title": _("Preset roles"),
|
||||
"name": "preset_roles",
|
||||
}
|
||||
)
|
||||
self.fields[key].help_text = ""
|
||||
self.fields[key].queryset = qset
|
||||
self.fields[key].label = _(category)
|
||||
@@ -57,33 +77,38 @@ class ApplicationForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(ApplicationForm, self).clean()
|
||||
for key in cleaned_data.keys():
|
||||
if 'preset_roles_' in key:
|
||||
cleaned_data['preset_roles'] = cleaned_data['preset_roles'] | cleaned_data[key]
|
||||
if "preset_roles_" in key:
|
||||
cleaned_data["preset_roles"] = (
|
||||
cleaned_data["preset_roles"] | cleaned_data[key]
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def clean_phone_number(self):
|
||||
"""Clean phone number field."""
|
||||
number = self.cleaned_data.get('phone_number')
|
||||
number = self.cleaned_data.get("phone_number")
|
||||
if number.isdigit():
|
||||
return number
|
||||
else:
|
||||
raise ValidationError(_('Invalid phone number'))
|
||||
raise ValidationError(_("Invalid phone number"))
|
||||
|
||||
def clean_custom_role_name(self):
|
||||
"""Check that no other custom role with same name exists."""
|
||||
custom_name = self.cleaned_data.get('custom_role_name')
|
||||
custom_name = self.cleaned_data.get("custom_role_name")
|
||||
if not CustomRole.objects.filter(name=custom_name).exists():
|
||||
return custom_name
|
||||
else:
|
||||
raise ValidationError(_('Custom role with the same name already exists.'))
|
||||
raise ValidationError(_("Custom role with the same name already exists."))
|
||||
|
||||
def non_role_fields(self):
|
||||
return [self.fields[k] for k in self.fields.keys() if k not in ["preset_roles", "custom_roles"]]
|
||||
return [
|
||||
self.fields[k]
|
||||
for k in self.fields.keys()
|
||||
if k not in ["preset_roles", "custom_roles"]
|
||||
]
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['name', 'email', 'message', 'parent']
|
||||
fields = ["name", "email", "message", "parent"]
|
||||
|
||||
@@ -12,82 +12,188 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('webapp', '0037_auto_20180125_2131'),
|
||||
("webapp", "0037_auto_20180125_2131"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CommentParent',
|
||||
name="CommentParent",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=255, verbose_name='Name')),
|
||||
('email', models.EmailField(default='', max_length=254, verbose_name='Email')),
|
||||
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(default="", max_length=255, verbose_name="Name"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(default="", max_length=254, verbose_name="Email"),
|
||||
),
|
||||
(
|
||||
"timestamp",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="Timestamp"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomRole',
|
||||
name="CustomRole",
|
||||
fields=[
|
||||
('baserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.BaseRole')),
|
||||
(
|
||||
"baserole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="webapp.BaseRole",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Custom kaehmy roles',
|
||||
'verbose_name': 'Custom kaehmy role',
|
||||
"verbose_name_plural": "Custom kaehmy roles",
|
||||
"verbose_name": "Custom kaehmy role",
|
||||
},
|
||||
bases=('webapp.baserole',),
|
||||
bases=("webapp.baserole",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PresetRole',
|
||||
name="PresetRole",
|
||||
fields=[
|
||||
('presetrole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.PresetRole')),
|
||||
(
|
||||
"presetrole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="webapp.PresetRole",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Preset kaehmy roles',
|
||||
'verbose_name': 'Preset kaehmy role',
|
||||
"verbose_name_plural": "Preset kaehmy roles",
|
||||
"verbose_name": "Preset kaehmy role",
|
||||
},
|
||||
bases=('webapp.presetrole',),
|
||||
bases=("webapp.presetrole",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TelegramChannel',
|
||||
name="TelegramChannel",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('channel_id', models.CharField(max_length=255, unique=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("channel_id", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Telegram channels',
|
||||
'verbose_name': 'Telegram channel',
|
||||
"verbose_name_plural": "Telegram channels",
|
||||
"verbose_name": "Telegram channel",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
name="Application",
|
||||
fields=[
|
||||
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
|
||||
('phone_number', models.CharField(default='', max_length=10, verbose_name='Phone number')),
|
||||
('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, 'N')], verbose_name='Year')),
|
||||
('text', models.TextField(default='', max_length=300, verbose_name='Text')),
|
||||
('custom_role_name', models.CharField(blank=True, max_length=255, verbose_name='Custom role name')),
|
||||
('custom_role_is_board', models.BooleanField(verbose_name='Board member')),
|
||||
('custom_roles', models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole')),
|
||||
('preset_roles', models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole')),
|
||||
(
|
||||
"commentparent_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone_number",
|
||||
models.CharField(
|
||||
default="", max_length=10, verbose_name="Phone number"
|
||||
),
|
||||
),
|
||||
(
|
||||
"year",
|
||||
models.IntegerField(
|
||||
choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "N")],
|
||||
verbose_name="Year",
|
||||
),
|
||||
),
|
||||
(
|
||||
"text",
|
||||
models.TextField(default="", max_length=300, verbose_name="Text"),
|
||||
),
|
||||
(
|
||||
"custom_role_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=255, verbose_name="Custom role name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"custom_role_is_board",
|
||||
models.BooleanField(verbose_name="Board member"),
|
||||
),
|
||||
(
|
||||
"custom_roles",
|
||||
models.ManyToManyField(
|
||||
blank=True, related_name="forms", to="kaehmy.CustomRole"
|
||||
),
|
||||
),
|
||||
(
|
||||
"preset_roles",
|
||||
models.ManyToManyField(
|
||||
blank=True, related_name="forms", to="kaehmy.PresetRole"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Kaehmylomakkeet',
|
||||
'verbose_name': 'Kaehmylomake',
|
||||
"verbose_name_plural": "Kaehmylomakkeet",
|
||||
"verbose_name": "Kaehmylomake",
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
bases=("kaehmy.commentparent",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
name="Comment",
|
||||
fields=[
|
||||
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
|
||||
('message', models.TextField(verbose_name='Message')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='kaehmy.CommentParent')),
|
||||
(
|
||||
"commentparent_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
("message", models.TextField(verbose_name="Message")),
|
||||
(
|
||||
"parent",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="messages",
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Kaehmykommentit',
|
||||
'verbose_name': 'Kaehmykommentti',
|
||||
"verbose_name_plural": "Kaehmykommentit",
|
||||
"verbose_name": "Kaehmykommentti",
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
bases=("kaehmy.commentparent",),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -7,26 +7,59 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('webapp', '0047_auto_20180710_2110'),
|
||||
('kaehmy', '0001_initial'),
|
||||
("webapp", "0047_auto_20180710_2110"),
|
||||
("kaehmy", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KaehmyBaseRole',
|
||||
name="KaehmyBaseRole",
|
||||
fields=[
|
||||
('baserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.BaseRole')),
|
||||
('category', models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('culture', 'Culture'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category')),
|
||||
(
|
||||
"baserole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="webapp.BaseRole",
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("corporate", "Corporate affairs"),
|
||||
("freshman", "Freshmen"),
|
||||
("international", "International"),
|
||||
("external", "External affairs"),
|
||||
("media", "Media"),
|
||||
("tech", "Technology"),
|
||||
("wellbeing", "Wellbeing"),
|
||||
("elepaja", "Elepaja"),
|
||||
("ceremonies", "Ceremonies"),
|
||||
("culture", "Culture"),
|
||||
("studies", "Studies"),
|
||||
("sosso", "Sössö magazine"),
|
||||
("alumni", "Alumni relations"),
|
||||
("others", "Others"),
|
||||
],
|
||||
default="others",
|
||||
max_length=255,
|
||||
verbose_name="Category",
|
||||
),
|
||||
),
|
||||
],
|
||||
bases=('webapp.baserole',),
|
||||
bases=("webapp.baserole",),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Application',
|
||||
name="Application",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='customrole',
|
||||
name="customrole",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='presetrole',
|
||||
name="presetrole",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -7,57 +7,113 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0002_auto_20180902_1929'),
|
||||
("kaehmy", "0002_auto_20180902_1929"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
name="Application",
|
||||
fields=[
|
||||
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
|
||||
('phone_number', models.CharField(default='', max_length=10, verbose_name='Phone number')),
|
||||
('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, 'N')], verbose_name='Year')),
|
||||
('text', models.TextField(default='', max_length=300, verbose_name='Text')),
|
||||
('custom_role_name', models.CharField(blank=True, max_length=255, verbose_name='Custom role name')),
|
||||
('custom_role_is_board', models.BooleanField(verbose_name='Board member')),
|
||||
(
|
||||
"commentparent_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone_number",
|
||||
models.CharField(
|
||||
default="", max_length=10, verbose_name="Phone number"
|
||||
),
|
||||
),
|
||||
(
|
||||
"year",
|
||||
models.IntegerField(
|
||||
choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "N")],
|
||||
verbose_name="Year",
|
||||
),
|
||||
),
|
||||
(
|
||||
"text",
|
||||
models.TextField(default="", max_length=300, verbose_name="Text"),
|
||||
),
|
||||
(
|
||||
"custom_role_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=255, verbose_name="Custom role name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"custom_role_is_board",
|
||||
models.BooleanField(verbose_name="Board member"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kaehmylomake',
|
||||
'verbose_name_plural': 'Kaehmylomakkeet',
|
||||
"verbose_name": "Kaehmylomake",
|
||||
"verbose_name_plural": "Kaehmylomakkeet",
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
bases=("kaehmy.commentparent",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomRole',
|
||||
name="CustomRole",
|
||||
fields=[
|
||||
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
|
||||
(
|
||||
"kaehmybaserole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.KaehmyBaseRole",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Custom kaehmy role',
|
||||
'verbose_name_plural': 'Custom kaehmy roles',
|
||||
"verbose_name": "Custom kaehmy role",
|
||||
"verbose_name_plural": "Custom kaehmy roles",
|
||||
},
|
||||
bases=('kaehmy.kaehmybaserole',),
|
||||
bases=("kaehmy.kaehmybaserole",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PresetRole',
|
||||
name="PresetRole",
|
||||
fields=[
|
||||
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
(
|
||||
"kaehmybaserole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.KaehmyBaseRole",
|
||||
),
|
||||
),
|
||||
("description", models.TextField(verbose_name="Description")),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Preset kaehmy role',
|
||||
'verbose_name_plural': 'Preset kaehmy roles',
|
||||
"verbose_name": "Preset kaehmy role",
|
||||
"verbose_name_plural": "Preset kaehmy roles",
|
||||
},
|
||||
bases=('kaehmy.kaehmybaserole',),
|
||||
bases=("kaehmy.kaehmybaserole",),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='custom_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole'),
|
||||
model_name="application",
|
||||
name="custom_roles",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="forms", to="kaehmy.CustomRole"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='preset_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole'),
|
||||
model_name="application",
|
||||
name="preset_roles",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="forms", to="kaehmy.PresetRole"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,32 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0003_auto_20180902_1943'),
|
||||
("kaehmy", "0003_auto_20180902_1943"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='kaehmybaserole',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category'),
|
||||
model_name="kaehmybaserole",
|
||||
name="category",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("corporate", "Corporate affairs"),
|
||||
("freshman", "Freshmen"),
|
||||
("international", "International"),
|
||||
("external", "External affairs"),
|
||||
("media", "Media"),
|
||||
("tech", "Technology"),
|
||||
("wellbeing", "Wellbeing"),
|
||||
("elepaja", "Elepaja"),
|
||||
("ceremonies", "Ceremonies"),
|
||||
("studies", "Studies"),
|
||||
("sosso", "Sössö magazine"),
|
||||
("alumni", "Alumni relations"),
|
||||
("others", "Others"),
|
||||
],
|
||||
default="others",
|
||||
max_length=255,
|
||||
verbose_name="Category",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0004_auto_20181018_2121'),
|
||||
("kaehmy", "0004_auto_20181018_2121"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='custom_role_is_board',
|
||||
field=models.BooleanField(blank=True, verbose_name='Board member'),
|
||||
model_name="application",
|
||||
name="custom_role_is_board",
|
||||
field=models.BooleanField(blank=True, verbose_name="Board member"),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 2.2.26 on 2022-01-12 20:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("kaehmy", "0005_auto_20190312_1458"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="TelegramChannel",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.14 on 2022-08-01 19:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("kaehmy", "0006_delete_telegramchannel"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="commentparent",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 3.2.14 on 2022-08-03 20:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("kaehmy", "0007_alter_commentparent_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="BaseRole",
|
||||
fields=[
|
||||
("id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("name", models.CharField(max_length=255, verbose_name="Name")),
|
||||
("is_board", models.BooleanField(verbose_name="Board member")),
|
||||
(
|
||||
"category",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("corporate", "Corporate affairs"),
|
||||
("freshman", "Freshmen"),
|
||||
("international", "International"),
|
||||
("external", "External affairs"),
|
||||
("media", "Media"),
|
||||
("tech", "Technology"),
|
||||
("wellbeing", "Wellbeing"),
|
||||
("elepaja", "Elepaja"),
|
||||
("ceremonies", "Ceremonies"),
|
||||
("studies", "Studies"),
|
||||
("sosso", "Sössö magazine"),
|
||||
("alumni", "Alumni relations"),
|
||||
("others", "Others"),
|
||||
],
|
||||
default="others",
|
||||
max_length=255,
|
||||
verbose_name="Category",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 2.2.28 on 2022-07-26 17:15
|
||||
|
||||
from unicodedata import category
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def copyBaseRolesToNewTable(apps, schema_editor):
|
||||
Old = apps.get_model("kaehmy", "KaehmyBaseRole")
|
||||
New = apps.get_model("kaehmy", "BaseRole")
|
||||
for bases in Old.objects.all():
|
||||
New.objects.create(
|
||||
id=bases.id,
|
||||
name=bases.name,
|
||||
is_board=bases.is_board,
|
||||
category=bases.category,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("kaehmy", "0008_baserole"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
copyBaseRolesToNewTable, reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 2.2.28 on 2022-07-26 17:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from sikweb.custom_operations import AlterModelBases
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("kaehmy", "0009_auto_20220726_2015"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AlterModelBases("customrole", (models.Model,)),
|
||||
AlterModelBases("presetrole", (models.Model,)),
|
||||
migrations.AlterField(
|
||||
model_name="customrole",
|
||||
name="kaehmybaserole_ptr",
|
||||
field=models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="BaseRole",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="presetrole",
|
||||
name="kaehmybaserole_ptr",
|
||||
field=models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="BaseRole",
|
||||
),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="customrole",
|
||||
old_name="kaehmybaserole_ptr",
|
||||
new_name="baserole_ptr",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="presetrole",
|
||||
old_name="kaehmybaserole_ptr",
|
||||
new_name="baserole_ptr",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.2.14 on 2022-08-03 20:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("kaehmy", "0010_auto_20220726_2033"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="KaehmyBaseRole",
|
||||
),
|
||||
]
|
||||
+82
-84
@@ -1,70 +1,71 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from webapp.utils import month_from_now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from auditlog.registry import auditlog
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from webapp.models import BaseRole
|
||||
import logging
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
VERBOSE_NAME = _("Kaehmy")
|
||||
|
||||
|
||||
# TODO: Move BaseRole to Kaehmt App; will fuck up the DB since table is removed, if no data migration is done before-hand.
|
||||
# Either reconstruct all kaehmy roles from scratch then, or do these migrations:
|
||||
# 1. Create table here
|
||||
# 2. Data migrate from webapp BaseRole to new kaehmy BaseRole
|
||||
# 3. Delete webapp BaseRole table
|
||||
class BaseRole(models.Model):
|
||||
"""Base model for occupations/roles."""
|
||||
|
||||
VERBOSE_NAME = _('Kaehmy')
|
||||
|
||||
|
||||
class KaehmyBaseRole(BaseRole):
|
||||
"""ABC"""
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(_("Name"), max_length=255)
|
||||
is_board = models.BooleanField(_("Board member"))
|
||||
|
||||
CATEGORIES = (
|
||||
('corporate', _('Corporate affairs')),
|
||||
('freshman', _('Freshmen')),
|
||||
('international', _('International')),
|
||||
('external', _('External affairs')),
|
||||
('media', _('Media')),
|
||||
('tech', _('Technology')),
|
||||
('wellbeing', _('Wellbeing')),
|
||||
('elepaja', _('Elepaja')),
|
||||
('ceremonies', _('Ceremonies')),
|
||||
('studies', _('Studies')),
|
||||
('sosso', _('Sössö magazine')),
|
||||
('alumni', _('Alumni relations')),
|
||||
('others', _('Others')),
|
||||
("board", _("Board")),
|
||||
("corporate", _("Corporate affairs")),
|
||||
("freshman", _("Freshmen")),
|
||||
("international", _("International")),
|
||||
("siwa", _("SIK's free time")),
|
||||
("media", _("Media")),
|
||||
("tech", _("Technology")),
|
||||
("wellbeing", _("Wellbeing")),
|
||||
("sikpaja", _("Sik-paja")),
|
||||
("ceremonies", _("Ceremonies")),
|
||||
("studies", _("Studies")),
|
||||
("sosso", _("Sössö magazine")),
|
||||
("pota", _("PoTa")),
|
||||
("alumni", _("Alumni relations")),
|
||||
("n", _("N")),
|
||||
("others", _("Others")),
|
||||
)
|
||||
category = models.CharField(_('Category'), choices=CATEGORIES, default='others', max_length=255)
|
||||
category = models.CharField(
|
||||
_("Category"), choices=CATEGORIES, default="others", max_length=255
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
n = self.name.capitalize()
|
||||
return "{} ({})".format(n, _("board member")) if self.is_board else n
|
||||
|
||||
|
||||
class PresetRole(KaehmyBaseRole):
|
||||
class PresetRole(BaseRole):
|
||||
"""Model for kaehmy role."""
|
||||
|
||||
description = models.TextField(_('Description'))
|
||||
description = models.TextField(_("Description"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Preset kaehmy role')
|
||||
verbose_name_plural = _('Preset kaehmy roles')
|
||||
verbose_name = _("Preset kaehmy role")
|
||||
verbose_name_plural = _("Preset kaehmy roles")
|
||||
|
||||
|
||||
class CustomRole(KaehmyBaseRole):
|
||||
class CustomRole(BaseRole):
|
||||
"""Model representing a user-specified custom occupation."""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Custom kaehmy role')
|
||||
verbose_name_plural = _('Custom kaehmy roles')
|
||||
verbose_name = _("Custom kaehmy role")
|
||||
verbose_name_plural = _("Custom kaehmy roles")
|
||||
|
||||
|
||||
class CommentParent(models.Model):
|
||||
|
||||
name = models.CharField(_('Name'), max_length=255, default='')
|
||||
email = models.EmailField(_('Email'), default='')
|
||||
timestamp = models.DateTimeField(_('Timestamp'), default=timezone.now)
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(_("Name"), max_length=255, default="")
|
||||
email = models.EmailField(_("Email"), default="")
|
||||
timestamp = models.DateTimeField(_("Timestamp"), default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
return 'Message parent #{}'.format(self.id)
|
||||
return "Message parent #{}".format(self.id)
|
||||
|
||||
|
||||
class Comment(CommentParent):
|
||||
@@ -75,11 +76,13 @@ class Comment(CommentParent):
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Kaehmykommentti')
|
||||
verbose_name_plural = _('Kaehmykommentit')
|
||||
verbose_name = _("Kaehmykommentti")
|
||||
verbose_name_plural = _("Kaehmykommentit")
|
||||
|
||||
message = models.TextField(_('Message'))
|
||||
parent = models.ForeignKey('CommentParent', related_name='messages', on_delete=models.CASCADE)
|
||||
message = models.TextField(_("Message"))
|
||||
parent = models.ForeignKey(
|
||||
"CommentParent", related_name="messages", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
|
||||
class Application(CommentParent):
|
||||
@@ -88,34 +91,36 @@ class Application(CommentParent):
|
||||
|
||||
Allows user to choose from existing roles or to create custom ones.
|
||||
"""
|
||||
|
||||
YEAR_CHOICES = (
|
||||
(1, '1'),
|
||||
(2, '2'),
|
||||
(3, '3'),
|
||||
(4, '4'),
|
||||
(5, 'N'),
|
||||
(1, "1"),
|
||||
(2, "2"),
|
||||
(3, "3"),
|
||||
(4, "4"),
|
||||
(5, "N"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Kaehmylomake')
|
||||
verbose_name_plural = _('Kaehmylomakkeet')
|
||||
verbose_name = _("Kaehmylomake")
|
||||
verbose_name_plural = _("Kaehmylomakkeet")
|
||||
|
||||
phone_number = models.CharField(
|
||||
_('Phone number'), max_length=10, default="")
|
||||
year = models.IntegerField(_('Year'), choices=YEAR_CHOICES)
|
||||
text = models.TextField(_('Text'), default="", max_length=300)
|
||||
phone_number = models.CharField(_("Phone number"), max_length=10, default="")
|
||||
year = models.IntegerField(_("Year"), choices=YEAR_CHOICES)
|
||||
text = models.TextField(_("Text"), default="", max_length=300)
|
||||
custom_role_name = models.CharField(
|
||||
_('Custom role name'), max_length=255, blank=True)
|
||||
custom_role_is_board = models.BooleanField(
|
||||
_('Board member'), blank=True)
|
||||
_("Custom role name"), max_length=255, blank=True
|
||||
)
|
||||
custom_role_is_board = models.BooleanField(_("Board member"), blank=True)
|
||||
custom_roles = models.ManyToManyField(
|
||||
'kaehmy.CustomRole', related_name='forms', blank=True)
|
||||
"kaehmy.CustomRole", related_name="forms", blank=True
|
||||
)
|
||||
preset_roles = models.ManyToManyField(
|
||||
'kaehmy.PresetRole', related_name='forms', blank=True)
|
||||
"kaehmy.PresetRole", related_name="forms", blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return model info."""
|
||||
return _('Kaehmy application: {}').format(self.name)
|
||||
return _("Kaehmy application: {}").format(self.name)
|
||||
|
||||
def comment_count(self):
|
||||
"""Count comments for kaehmy."""
|
||||
@@ -137,34 +142,27 @@ class Application(CommentParent):
|
||||
presets = [r.name.capitalize() for r in self.preset_roles.filter(is_board=True)]
|
||||
customs = [r.name.capitalize() for r in self.custom_roles.filter(is_board=True)]
|
||||
combined = presets + customs
|
||||
return _('Board: {}').format(', '.join(combined)) if len(combined) > 0 else ''
|
||||
return _("Board: {}").format(", ".join(combined)) if len(combined) > 0 else ""
|
||||
|
||||
def official_roles(self):
|
||||
presets = [r.name.capitalize() for r in self.preset_roles.filter(is_board=False)]
|
||||
customs = [r.name.capitalize() for r in self.custom_roles.filter(is_board=False)]
|
||||
presets = [
|
||||
r.name.capitalize() for r in self.preset_roles.filter(is_board=False)
|
||||
]
|
||||
customs = [
|
||||
r.name.capitalize() for r in self.custom_roles.filter(is_board=False)
|
||||
]
|
||||
combined = presets + customs
|
||||
return _('Official: {}').format(', '.join(combined)) if len(combined) > 0 else ''
|
||||
return (
|
||||
_("Official: {}").format(", ".join(combined)) if len(combined) > 0 else ""
|
||||
)
|
||||
|
||||
def all_roles(self):
|
||||
presets = [r.name.capitalize() for r in self.preset_roles.all()]
|
||||
customs = [r.name.capitalize() for r in self.custom_roles.all()]
|
||||
combined = presets + customs
|
||||
return ', '.join(combined) if len(combined) > 0 else ''
|
||||
return ", ".join(combined) if len(combined) > 0 else ""
|
||||
|
||||
def has_any_board_role(self):
|
||||
return self.preset_roles.filter(is_board=True).exists() or self.custom_roles.filter(is_board=True)
|
||||
|
||||
|
||||
# Telegram channel entry for Kaehmys
|
||||
class TelegramChannel(models.Model):
|
||||
"""Model containing the channel id of a Telegram chat"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Telegram channel')
|
||||
verbose_name_plural = _('Telegram channels')
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
channel_id = models.CharField(max_length=255, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return 'Telegram channel: "{}"'.format(self.name)
|
||||
return self.preset_roles.filter(
|
||||
is_board=True
|
||||
).exists() or self.custom_roles.filter(is_board=True)
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 1000px;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
div.tooltip-inner {
|
||||
max-width: 25rem;
|
||||
}
|
||||
@@ -28,6 +22,10 @@ div.tooltip-inner {
|
||||
.kaehmy-content {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
}
|
||||
|
||||
footer {
|
||||
/* position: absolute; */
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px; /* Set the fixed height of the footer here */
|
||||
/* line-height: 60px; /* Vertically center the text there */
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
footer .container .col .nav .nav-item {
|
||||
@@ -26,6 +22,7 @@ footer .container .col .nav .nav-item {
|
||||
|
||||
.lang-select {
|
||||
width: 10rem;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
.header-content {
|
||||
|
||||
.kaehmy-header {
|
||||
background-color: #0c2938;
|
||||
}
|
||||
|
||||
.header-content .logo {
|
||||
.kaehmy-header-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.header-content .logo img {
|
||||
display: block;
|
||||
height: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.kaehmy-banner {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
.kaehmy_header-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: #0c2938;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kaehmy_header {
|
||||
margin-bottom: 331px;
|
||||
}
|
||||
}
|
||||
|
||||
.kaehmy-banner-image {
|
||||
width: 100%;
|
||||
max-height: 10rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
place-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
.kaehmy_navigation {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.navbar-border {
|
||||
border-bottom: 2px solid #282b3b;
|
||||
}
|
||||
|
||||
.navbar-light .navbar-nav .nav-link {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 88 KiB |
+9
-3
@@ -1,6 +1,6 @@
|
||||
import django_tables2 as tables
|
||||
from django.db.models import Count, Q
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from kaehmy.models import Application
|
||||
|
||||
@@ -8,6 +8,12 @@ from kaehmy.models import Application
|
||||
class ExportTable(tables.Table):
|
||||
class Meta:
|
||||
model = Application
|
||||
exclude = ['text', 'messageparent_ptr', 'custom_role_name', 'custom_role_is_board', 'timestamp']
|
||||
exclude = [
|
||||
"text",
|
||||
"messageparent_ptr",
|
||||
"custom_role_name",
|
||||
"custom_role_is_board",
|
||||
"timestamp",
|
||||
]
|
||||
|
||||
all_roles = tables.Column(verbose_name=_('Roles'), orderable=False)
|
||||
all_roles = tables.Column(verbose_name=_("Roles"), orderable=False)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
|
||||
<footer style="text-align: center">
|
||||
<div>
|
||||
<form class="lang-form form" action="{% url 'set_language' %}" method="post">{% csrf_token %}
|
||||
<span>
|
||||
<input name="next" type="hidden" value="{{ redirect_to }}" />
|
||||
<select onchange="this.form.submit()" class="lang-select form-control" name="language">
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
{{ language.name_local }} ({{ language.code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
</form>
|
||||
<span>{% trans "Copyright Aalto-yliopiston Sähköinsinöörikilta ry" %} {% now 'Y' %}</span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,7 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="kaehmy_header-content">
|
||||
<div class="kaehmy-banner logo">
|
||||
<a href="/kaehmy"><img class="kaehmy-banner-image" src="/static/kaehmy/img/kaehmy_banner.png" alt="Aalto-yliopiston Sähköinsinöörikilta ry"></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
'''
|
||||
A telegram bot api for whatever purposes.
|
||||
TODO: kaehmy app is definitely not correct place for this
|
||||
'''
|
||||
import logging
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from kaehmy.models import TelegramChannel
|
||||
|
||||
|
||||
class TelegramBot:
|
||||
'''
|
||||
A telegram bot api for whatever purposes
|
||||
Currently only able to broadcast stuff to all registered
|
||||
channels using broadcast method.
|
||||
'''
|
||||
|
||||
def __init__(self, api_token=None):
|
||||
|
||||
self.api_token = api_token or settings.TELEGRAM_BOT_TOKEN
|
||||
self.send_message_url = "https://api.telegram.org/bot{}/sendMessage".format(self.api_token)
|
||||
|
||||
def broadcast(self, message):
|
||||
channels_ids = TelegramChannel.objects.values_list("channel_id", flat=True)
|
||||
for id_ in channels_ids:
|
||||
self.send_message(id_, message)
|
||||
|
||||
def send_message(self, channel_id, message):
|
||||
'''
|
||||
Send message to a chat with given channel_id
|
||||
'''
|
||||
data = {
|
||||
'chat_id': channel_id,
|
||||
'text': message,
|
||||
'parse_mode': 'Markdown'
|
||||
}
|
||||
resp = requests.post(self.send_message_url, json=data)
|
||||
logging.debug(resp.content)
|
||||
@@ -6,13 +6,13 @@ from kaehmy.models import PresetRole, CustomRole
|
||||
|
||||
@register(PresetRole)
|
||||
class PresetRoleTranslationOptions(TranslationOptions):
|
||||
""" Class for PresetRole translation options"""
|
||||
"""Class for PresetRole translation options"""
|
||||
|
||||
fields = ()
|
||||
|
||||
|
||||
@register(CustomRole)
|
||||
class CustomRoleTranslationOptions(TranslationOptions):
|
||||
""" Class for CustomROle translation options"""
|
||||
"""Class for CustomROle translation options"""
|
||||
|
||||
fields = ()
|
||||
|
||||
+9
-8
@@ -1,8 +1,8 @@
|
||||
"""Kaehmy urls."""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from kaehmy.views import view
|
||||
from kaehmy.views import list_view
|
||||
@@ -13,14 +13,15 @@ from kaehmy.views import export_view
|
||||
|
||||
urlpatterns = [
|
||||
# kaehmy
|
||||
url(r'^new', view),
|
||||
url(r'^submit', submit),
|
||||
url(r'^add_comment', comment),
|
||||
url(r'^statistics', statistics_view),
|
||||
url(r'^export', export_view),
|
||||
url(r'^$', list_view),
|
||||
re_path(r"^new", view),
|
||||
re_path(r"^submit", submit),
|
||||
re_path(r"^add_comment", comment),
|
||||
re_path(r"^statistics", statistics_view),
|
||||
re_path(r"^export", export_view),
|
||||
re_path(r"^$", list_view),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
+74
-77
@@ -1,22 +1,20 @@
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import login, logout, authenticate
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.contrib.auth.decorators import permission_required, login_required
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from dealer.git import git
|
||||
from sikweb.settings import URL
|
||||
|
||||
from members.views.utils import *
|
||||
from kaehmy.models import Application, CustomRole, PresetRole, TelegramChannel
|
||||
from kaehmy.models import Application, CustomRole, PresetRole
|
||||
from kaehmy.forms import ApplicationForm, CommentForm
|
||||
from kaehmy.tables import ExportTable
|
||||
from webapp.utils import send_email
|
||||
from webapp.models import processHooks
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -24,28 +22,38 @@ from webapp.utils import send_email
|
||||
def list_view(request, *args, **kwargs):
|
||||
"""Kaehmy application list"""
|
||||
|
||||
role_filter = request.GET.get('role', None)
|
||||
if role_filter is not None and str(role_filter) != '-1':
|
||||
applications = Application.objects.filter(custom_roles__id=role_filter) | Application.objects.filter(preset_roles__id=role_filter)
|
||||
role_filter = request.GET.get("role", None)
|
||||
if role_filter is not None and str(role_filter) != "-1":
|
||||
applications = Application.objects.filter(
|
||||
custom_roles__id=role_filter
|
||||
) | Application.objects.filter(preset_roles__id=role_filter)
|
||||
else:
|
||||
applications = Application.objects.all()
|
||||
|
||||
applications = applications.order_by('-timestamp')
|
||||
filter_options_preset = PresetRole.objects.annotate(form_count=Count('forms')).filter(form_count__gt=0)
|
||||
filter_options_preset_list = [(r.id, r.name, r.form_count) for r in filter_options_preset]
|
||||
filter_options_custom = CustomRole.objects.annotate(form_count=Count('forms')).filter(form_count__gt=0)
|
||||
filter_options_custom_list = [(r.id, r.name, r.form_count) for r in filter_options_custom]
|
||||
applications = applications.order_by("-timestamp")
|
||||
filter_options_preset = PresetRole.objects.annotate(
|
||||
form_count=Count("forms")
|
||||
).filter(form_count__gt=0)
|
||||
filter_options_preset_list = [
|
||||
(r.id, r.name, r.form_count) for r in filter_options_preset
|
||||
]
|
||||
filter_options_custom = CustomRole.objects.annotate(
|
||||
form_count=Count("forms")
|
||||
).filter(form_count__gt=0)
|
||||
filter_options_custom_list = [
|
||||
(r.id, r.name, r.form_count) for r in filter_options_custom
|
||||
]
|
||||
|
||||
filter_options = filter_options_preset_list + filter_options_custom_list
|
||||
filter_options.sort(key=lambda f: f[1])
|
||||
|
||||
context = {
|
||||
'applications': applications,
|
||||
'application_count': len(applications),
|
||||
'filter_options': filter_options
|
||||
"applications": applications,
|
||||
"application_count": len(applications),
|
||||
"filter_options": filter_options,
|
||||
}
|
||||
|
||||
return render(request, 'kaehmy:list.html', context)
|
||||
return render(request, "kaehmy/list.html", context)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -56,23 +64,22 @@ def comment(request, *args, **kwargs):
|
||||
form = CommentForm(request.POST)
|
||||
if form.is_valid():
|
||||
comment = form.save()
|
||||
email = comment.parent.email
|
||||
name = comment.name
|
||||
url = f"https://{URL}/kaehmy"
|
||||
|
||||
subject = 'Kaehmyysi tai kommenttiisi on vastattu!'
|
||||
body = (f'{name.capitalize()} on vastannut kaehmyhakemukseesi tai kommenttiisi kaehmypalvelussa.\r\n\r\n'
|
||||
'Käy lukemassa viesti osoitteessa https://{URL}/kaehmy')
|
||||
to_email = comment.parent.email
|
||||
subject = "Kaehmyysi tai kommenttiisi on vastattu!"
|
||||
message = render_to_string(
|
||||
"kaehmy/email_comment.html", {"name": name, "url": url}
|
||||
)
|
||||
|
||||
send_email(email, subject, body)
|
||||
logging.debug(
|
||||
f'Sent kaehmy comment email to recipient <{email}>')
|
||||
send_email(to=to_email, subject=subject, body=message, html=True)
|
||||
logging.debug(f"Sent kaehmy comment email to recipient <{to_email}>")
|
||||
|
||||
return redirect('/kaehmy')
|
||||
return redirect("/kaehmy")
|
||||
else:
|
||||
context = {
|
||||
'error': form.errors
|
||||
}
|
||||
return render(request, 'kaehmy:error.html', context)
|
||||
context = {"error": form.errors}
|
||||
return render(request, "kaehmy/error.html", context)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -87,24 +94,24 @@ def statistics_view(request, *args, **kwargs):
|
||||
|
||||
for preset in preset_roles:
|
||||
people = [form.name for form in preset.forms.all()]
|
||||
role_list.append((preset.name, len(people), ', '.join(people)))
|
||||
role_list.append((preset.name, len(people), ", ".join(people)))
|
||||
for custom in custom_roles:
|
||||
people = [form.name for form in custom.forms.all()]
|
||||
role_list.append((custom.name, len(people), ', '.join(people)))
|
||||
role_list.append((custom.name, len(people), ", ".join(people)))
|
||||
|
||||
context = {
|
||||
'applications': applications,
|
||||
'application_count': len(applications),
|
||||
'role_list': role_list
|
||||
"applications": applications,
|
||||
"application_count": len(applications),
|
||||
"role_list": role_list,
|
||||
}
|
||||
return render(request, 'kaehmy:statistics.html', context)
|
||||
return render(request, "kaehmy/statistics.html", context)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def view(request, *args, **kwargs):
|
||||
"""Render Kaehmy form page."""
|
||||
form = ApplicationForm()
|
||||
return render(request, 'kaehmy:kaehmy.html', {'form': form})
|
||||
return render(request, "kaehmy/kaehmy.html", {"form": form})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -114,56 +121,46 @@ def submit(request, *args, **kwargs):
|
||||
form = ApplicationForm(request.POST)
|
||||
if form.is_valid():
|
||||
application = form.save()
|
||||
custom_name = form.cleaned_data.get('custom_role_name')
|
||||
custom_is_board = form.cleaned_data.get('custom_role_is_board')
|
||||
custom_name = form.cleaned_data.get("custom_role_name")
|
||||
custom_is_board = form.cleaned_data.get("custom_role_is_board")
|
||||
kaehmybot_allowed = form.cleaned_data.get("kaehmybot") == "1"
|
||||
|
||||
if len(custom_name) > 0:
|
||||
custom_role = CustomRole(
|
||||
name=custom_name, is_board=custom_is_board)
|
||||
custom_role = CustomRole(name=custom_name, is_board=custom_is_board)
|
||||
custom_role.save()
|
||||
application.custom_roles.add(custom_role)
|
||||
|
||||
url = f'https://{URL}/kaehmy'
|
||||
url = f"https://{URL}/kaehmy"
|
||||
name = form.cleaned_data.get("name", "Anonymous")
|
||||
|
||||
email = form.cleaned_data.get('email', '')
|
||||
name = form.cleaned_data.get('name', 'Anonymous')
|
||||
subject = 'Arwokas kirjattu kirje mahdolliselle tulewalle kiltahenkilölle'
|
||||
body = ('Moikka {}!\r\n\r\nHienoa, että kilta kiinnostaa! Kaehmysi on vastaanotettu.\r\n'
|
||||
'Mahdollisista kommenteista tulee ilmoitus sähköpostitse.\r\n\r\n'
|
||||
'Käy katsomassa kaehmytilanne osoitteessa {}').format(name, url)
|
||||
to_email = form.cleaned_data.get("email", "")
|
||||
subject = "Arwokas kirjattu kirje mahdolliselle tulewalle kiltahenkilölle"
|
||||
message = render_to_string(
|
||||
"kaehmy/email_kaehmy.html", {"name": name, "url": url}
|
||||
)
|
||||
|
||||
send_email(email, subject, body)
|
||||
logging.debug('Sent kaehmy email to recipient <{}>'.format(email))
|
||||
|
||||
CHAT_IDS = [channel.channel_id for channel in TelegramChannel.objects.all()]
|
||||
for chat_id in CHAT_IDS:
|
||||
tg_string = 'https://api.telegram.org/bot{}/sendMessage?chat_id={}&text={}'.format(
|
||||
settings.TELEGRAM_BOT_TOKEN,
|
||||
chat_id,
|
||||
'Uusi New kaehmy! {} -> {}'.format(name, url)
|
||||
)
|
||||
response = requests.get(tg_string).json()
|
||||
logging.debug('Telegram API response:\n{}'.format(response))
|
||||
logging.debug('Sent kaehmy announcement to {} channels.'.format(len(CHAT_IDS)))
|
||||
send_email(to=to_email, subject=subject, body=message, html=True)
|
||||
logging.debug(f"Sent kaehmy email to recipient <{to_email}>")
|
||||
|
||||
processHooks(message=f"Uusi New kaehmy! {name} -> {url}", eventType="kaehmy")
|
||||
else:
|
||||
context = {
|
||||
'error': form.errors
|
||||
}
|
||||
return render(request, 'kaehmy:error.html', context)
|
||||
return HttpResponseRedirect('/kaehmy')
|
||||
context = {"error": form.errors}
|
||||
return render(request, "kaehmy/error.html", context)
|
||||
return HttpResponseRedirect("/kaehmy")
|
||||
|
||||
|
||||
@require_http_methods(['GET'])
|
||||
@login_required(login_url='/admin/login')
|
||||
@require_http_methods(["GET"])
|
||||
@login_required(login_url="/admin/login")
|
||||
def export_view(request, *args, **kwargs):
|
||||
def make_table(queryset):
|
||||
table = ExportTable(queryset,
|
||||
request=request,
|
||||
exclude=['id'],
|
||||
attrs={'class': 'table table-bordered table-hover'})
|
||||
table = ExportTable(
|
||||
queryset,
|
||||
request=request,
|
||||
exclude=["id"],
|
||||
attrs={"class": "table table-bordered table-hover"},
|
||||
)
|
||||
|
||||
table.paginate(page=request.GET.get('page', 1), per_page=9999)
|
||||
table.paginate(page=request.GET.get("page", 1), per_page=9999)
|
||||
table_html = convert_table_to_html(table, request)
|
||||
return table_html
|
||||
|
||||
@@ -172,7 +169,7 @@ def export_view(request, *args, **kwargs):
|
||||
board = filter(lambda q: q.has_any_board_role(), kaehmys)
|
||||
|
||||
context = {
|
||||
'non_board_table': make_table(non_board),
|
||||
'board_table': make_table(board),
|
||||
"non_board_table": make_table(non_board),
|
||||
"board_table": make_table(board),
|
||||
}
|
||||
return render(request, 'kaehmy:export.html', context)
|
||||
return render(request, "kaehmy/export.html", context)
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# Linux/Mac installation instructions
|
||||
|
||||
## Install dependencies
|
||||
|
||||
### Dependency list
|
||||
|
||||
* Python >3.5
|
||||
* PostgreSQL >9.5
|
||||
* pip3
|
||||
* virtualenv
|
||||
* npm
|
||||
|
||||
Install with apt:
|
||||
|
||||
```bash
|
||||
sudo apt install python3
|
||||
sudo apt install python3-pip
|
||||
sudo apt install postgresql
|
||||
sudo pip3 install virtualenv
|
||||
sudo apt install npm
|
||||
```
|
||||
|
||||
More info about PostgreSQL at:
|
||||
[https://www.postgresql.org/](https://www.postgresql.org)
|
||||
|
||||
These packages might be needed on certain platforms:
|
||||
|
||||
* python3-dev
|
||||
* libffi-dev
|
||||
* python3-cffi
|
||||
* libssl-dev
|
||||
|
||||
## Create a virtual environment for python
|
||||
|
||||
Create a virtualenv in the parent directory.
|
||||
|
||||
```bash
|
||||
virtualenv -p python3 ../virtualenv.sikweb
|
||||
```
|
||||
|
||||
## Activate virtualenv
|
||||
|
||||
Assuming we are at the root of this repository and virtualenv is one level above.
|
||||
|
||||
```bash
|
||||
. ../virtualenv.sikweb/bin/activate
|
||||
```
|
||||
|
||||
## Run install wizard
|
||||
|
||||
Run the install wizard with
|
||||
|
||||
```bash
|
||||
bash setup.sh
|
||||
```
|
||||
|
||||
and follow the instructions.
|
||||
|
||||
## Done
|
||||
|
||||
## In case of error on macOS Mojave 10.14
|
||||
|
||||
If you get an error saying
|
||||
|
||||
```bash
|
||||
The headers or library files could not be found for zlib,
|
||||
a required dependency when compiling Pillow from source.
|
||||
```
|
||||
|
||||
run
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target /
|
||||
```
|
||||
Binary file not shown.
+857
-809
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+662
-645
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -8,4 +8,4 @@ admin.site.register(Member)
|
||||
admin.site.register(Request)
|
||||
admin.site.register(Payment)
|
||||
|
||||
admin.site.site_header = 'SIK Admin'
|
||||
admin.site.site_header = "SIK Admin"
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ from django.apps import AppConfig
|
||||
class MembersConfig(AppConfig):
|
||||
"""Class for Members app configurations."""
|
||||
|
||||
name = 'members'
|
||||
name = "members"
|
||||
|
||||
+26
-23
@@ -2,7 +2,7 @@
|
||||
|
||||
from django import forms
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from members.models import Member, Payment, Request
|
||||
|
||||
@@ -23,7 +23,7 @@ class MemberForm(forms.ModelForm):
|
||||
"""Meta for Member model form."""
|
||||
|
||||
model = Member
|
||||
fields = ['first_name', 'last_name', 'email', 'POR', 'AYY', 'jas']
|
||||
fields = ["first_name", "last_name", "email", "POR", "AYY", "jas"]
|
||||
|
||||
class ImportResult:
|
||||
def __init__(self, members, payments):
|
||||
@@ -32,22 +32,27 @@ class MemberForm(forms.ModelForm):
|
||||
|
||||
def _clean_boolean_field(self, key):
|
||||
value = self.data.get(key, None)
|
||||
if value in ['1', '0']:
|
||||
if value in ["1", "0"]:
|
||||
return bool(int(value))
|
||||
else:
|
||||
return value == 'on'
|
||||
return value == "on"
|
||||
|
||||
def clean_jas(self):
|
||||
return self._clean_boolean_field('jas')
|
||||
return self._clean_boolean_field("jas")
|
||||
|
||||
def clean_AYY(self):
|
||||
return self._clean_boolean_field('AYY')
|
||||
return self._clean_boolean_field("AYY")
|
||||
|
||||
@staticmethod
|
||||
def csv_to_models(data, payment_source='AYY', delimiter=','):
|
||||
clean_data = data.strip().split('\n')
|
||||
clean_data = [row.rstrip(',').rstrip('\r').strip() for row in clean_data]
|
||||
csv_reader = csv.DictReader(clean_data, fieldnames=MemberForm.Meta.fields, delimiter=delimiter, quoting=csv.QUOTE_NONE)
|
||||
def csv_to_models(data, payment_source="AYY", delimiter=","):
|
||||
clean_data = data.strip().split("\n")
|
||||
clean_data = [row.rstrip(",").rstrip("\r").strip() for row in clean_data]
|
||||
csv_reader = csv.DictReader(
|
||||
clean_data,
|
||||
fieldnames=MemberForm.Meta.fields,
|
||||
delimiter=delimiter,
|
||||
quoting=csv.QUOTE_NONE,
|
||||
)
|
||||
|
||||
members = []
|
||||
payments = []
|
||||
@@ -57,10 +62,10 @@ class MemberForm(forms.ModelForm):
|
||||
line[key] = value.strip()
|
||||
except AttributeError as ex:
|
||||
logging.error('Invalid line in CSV: "{}"'.format(line))
|
||||
logging.error('Delimiter: {}'.format(delimiter))
|
||||
logging.error("Delimiter: {}".format(delimiter))
|
||||
raise
|
||||
|
||||
email = line['email']
|
||||
email = line["email"]
|
||||
member_exists = False
|
||||
if Member.objects.filter(email=email).exists():
|
||||
member_exists = True
|
||||
@@ -76,9 +81,9 @@ class MemberForm(forms.ModelForm):
|
||||
else:
|
||||
member = Member.objects.get(email=email)
|
||||
payment_data = {
|
||||
'source': payment_source,
|
||||
'member': member.id,
|
||||
'date': timezone.now(),
|
||||
"source": payment_source,
|
||||
"member": member.id,
|
||||
"date": timezone.now(),
|
||||
}
|
||||
form = PaymentForm(payment_data)
|
||||
if not form.is_valid():
|
||||
@@ -95,17 +100,15 @@ class PaymentForm(forms.ModelForm):
|
||||
|
||||
member = forms.ModelChoiceField(
|
||||
queryset=Member.objects.all(),
|
||||
widget=autocomplete.ModelSelect2(url='member-autocomplete')
|
||||
widget=autocomplete.ModelSelect2(url="member-autocomplete"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
"""Meta for Payment model form."""
|
||||
|
||||
model = Payment
|
||||
fields = ['date', 'source', 'member']
|
||||
labels = {
|
||||
'member': _('Member')
|
||||
}
|
||||
fields = ["date", "source", "member"]
|
||||
labels = {"member": _("Member")}
|
||||
|
||||
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
@@ -115,13 +118,13 @@ class ApplicationForm(forms.ModelForm):
|
||||
"""Meta for application model form."""
|
||||
|
||||
model = Request
|
||||
fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR']
|
||||
fields = ["first_name", "last_name", "email", "AYY", "jas", "POR"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ApplicationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['AYY'].label = _("I'm a member of AYY")
|
||||
self.fields['jas'].label = _("I want to receive a weekly newsletter")
|
||||
self.fields["AYY"].label = _("I'm a member of AYY")
|
||||
self.fields["jas"].label = _("I want to receive a weekly newsletter")
|
||||
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
|
||||
@@ -12,29 +12,52 @@ class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Member',
|
||||
name="Member",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=127)),
|
||||
('last_name', models.CharField(max_length=127)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('POR', models.CharField(max_length=255)),
|
||||
('AYY', models.BooleanField(default=False)),
|
||||
('jas', models.BooleanField(default=False)),
|
||||
('created', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('paid', models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0))),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("first_name", models.CharField(max_length=127)),
|
||||
("last_name", models.CharField(max_length=127)),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("POR", models.CharField(max_length=255)),
|
||||
("AYY", models.BooleanField(default=False)),
|
||||
("jas", models.BooleanField(default=False)),
|
||||
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
(
|
||||
"paid",
|
||||
models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0)),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MemberRequest',
|
||||
name="MemberRequest",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.Member')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"member",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="members.Member"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,13 +9,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0001_initial'),
|
||||
("members", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='paid',
|
||||
model_name="member",
|
||||
name="paid",
|
||||
field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 2, 0)),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -11,57 +11,81 @@ import django.utils.timezone
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0002_auto_20170329_1857'),
|
||||
("members", "0002_auto_20170329_1857"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
name="Payment",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateTimeField(default=datetime.datetime(1970, 1, 1, 2, 0))),
|
||||
('source', models.CharField(max_length=255)),
|
||||
('first_name', models.CharField(max_length=255)),
|
||||
('last_name', models.CharField(max_length=255)),
|
||||
('email', models.EmailField(max_length=255)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date",
|
||||
models.DateTimeField(default=datetime.datetime(1970, 1, 1, 2, 0)),
|
||||
),
|
||||
("source", models.CharField(max_length=255)),
|
||||
("first_name", models.CharField(max_length=255)),
|
||||
("last_name", models.CharField(max_length=255)),
|
||||
("email", models.EmailField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Request',
|
||||
name="Request",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=127)),
|
||||
('last_name', models.CharField(max_length=127)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('POR', models.CharField(default='ei_tiedossa', max_length=255)),
|
||||
('AYY', models.BooleanField(default=False)),
|
||||
('jas', models.BooleanField(default=False)),
|
||||
('submitted', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("first_name", models.CharField(max_length=127)),
|
||||
("last_name", models.CharField(max_length=127)),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("POR", models.CharField(default="ei_tiedossa", max_length=255)),
|
||||
("AYY", models.BooleanField(default=False)),
|
||||
("jas", models.BooleanField(default=False)),
|
||||
("submitted", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='memberrequest',
|
||||
name='member',
|
||||
model_name="memberrequest",
|
||||
name="member",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='POR',
|
||||
field=models.CharField(default='ei_tiedossa', max_length=255),
|
||||
model_name="member",
|
||||
name="POR",
|
||||
field=models.CharField(default="ei_tiedossa", max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='paid',
|
||||
model_name="member",
|
||||
name="paid",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='MemberRequest',
|
||||
name="MemberRequest",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='payment',
|
||||
name='member',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='members.Member'),
|
||||
model_name="payment",
|
||||
name="member",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="members.Member",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,80 +8,80 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0003_auto_20170329_1928'),
|
||||
("members", "0003_auto_20170329_1928"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='payment',
|
||||
name='email',
|
||||
model_name="payment",
|
||||
name="email",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='payment',
|
||||
name='first_name',
|
||||
model_name="payment",
|
||||
name="first_name",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='payment',
|
||||
name='last_name',
|
||||
model_name="payment",
|
||||
name="last_name",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='AYY',
|
||||
field=models.BooleanField(default=False, verbose_name='AYY'),
|
||||
model_name="member",
|
||||
name="AYY",
|
||||
field=models.BooleanField(default=False, verbose_name="AYY"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='POR',
|
||||
field=models.CharField(max_length=255, verbose_name='Place of residence'),
|
||||
model_name="member",
|
||||
name="POR",
|
||||
field=models.CharField(max_length=255, verbose_name="Place of residence"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, verbose_name='Email'),
|
||||
model_name="member",
|
||||
name="email",
|
||||
field=models.EmailField(max_length=254, verbose_name="Email"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='first_name',
|
||||
field=models.CharField(max_length=127, verbose_name='First name'),
|
||||
model_name="member",
|
||||
name="first_name",
|
||||
field=models.CharField(max_length=127, verbose_name="First name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='jas',
|
||||
field=models.BooleanField(default=False, verbose_name='JAS'),
|
||||
model_name="member",
|
||||
name="jas",
|
||||
field=models.BooleanField(default=False, verbose_name="JAS"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='last_name',
|
||||
field=models.CharField(max_length=127, verbose_name='Last name'),
|
||||
model_name="member",
|
||||
name="last_name",
|
||||
field=models.CharField(max_length=127, verbose_name="Last name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='AYY',
|
||||
field=models.BooleanField(default=False, verbose_name='AYY'),
|
||||
model_name="request",
|
||||
name="AYY",
|
||||
field=models.BooleanField(default=False, verbose_name="AYY"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='POR',
|
||||
field=models.CharField(max_length=255, verbose_name='Place of residence'),
|
||||
model_name="request",
|
||||
name="POR",
|
||||
field=models.CharField(max_length=255, verbose_name="Place of residence"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, verbose_name='Email'),
|
||||
model_name="request",
|
||||
name="email",
|
||||
field=models.EmailField(max_length=254, verbose_name="Email"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='first_name',
|
||||
field=models.CharField(max_length=127, verbose_name='First name'),
|
||||
model_name="request",
|
||||
name="first_name",
|
||||
field=models.CharField(max_length=127, verbose_name="First name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='jas',
|
||||
field=models.BooleanField(default=False, verbose_name='JAS'),
|
||||
model_name="request",
|
||||
name="jas",
|
||||
field=models.BooleanField(default=False, verbose_name="JAS"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='last_name',
|
||||
field=models.CharField(max_length=127, verbose_name='Last name'),
|
||||
model_name="request",
|
||||
name="last_name",
|
||||
field=models.CharField(max_length=127, verbose_name="Last name"),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,22 +9,31 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0004_auto_20170512_1454'),
|
||||
("members", "0004_auto_20170512_1454"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='member',
|
||||
name='paid',
|
||||
model_name="member",
|
||||
name="paid",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2017, 5, 13, 10, 29, 50, 116064)),
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime(2017, 5, 13, 10, 29, 50, 116064)
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='source',
|
||||
field=models.CharField(choices=[('AYY', 'AYY'), ('cash', 'Cash'), ('bank_transfer', 'Bank transfer')], max_length=255),
|
||||
model_name="payment",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("AYY", "AYY"),
|
||||
("cash", "Cash"),
|
||||
("bank_transfer", "Bank transfer"),
|
||||
],
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,13 +9,15 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0005_auto_20170513_1029'),
|
||||
("members", "0005_auto_20170513_1029"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2017, 5, 17, 13, 9, 21, 49238)),
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime(2017, 5, 17, 13, 9, 21, 49238)
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,13 +9,15 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0006_auto_20170517_1309'),
|
||||
("members", "0006_auto_20170517_1309"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2017, 5, 18, 15, 38, 7, 668612)),
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime(2017, 5, 18, 15, 38, 7, 668612)
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,18 +9,18 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0007_auto_20170518_1538'),
|
||||
("members", "0007_auto_20170518_1538"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='created',
|
||||
model_name="member",
|
||||
name="created",
|
||||
field=models.DateTimeField(default=datetime.datetime.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='date',
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateTimeField(default=datetime.datetime.now),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,13 +9,15 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0008_auto_20170518_1540'),
|
||||
("members", "0008_auto_20170518_1540"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='created',
|
||||
field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Created'),
|
||||
model_name="member",
|
||||
name="created",
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime.now, verbose_name="Created"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,18 +9,28 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0009_auto_20170526_1903'),
|
||||
("members", "0009_auto_20170526_1903"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='date',
|
||||
field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Date'),
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateTimeField(
|
||||
default=datetime.datetime.now, verbose_name="Date"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='source',
|
||||
field=models.CharField(choices=[('AYY', 'AYY'), ('cash', 'Cash'), ('bank_transfer', 'Bank transfer')], max_length=255, verbose_name='Source'),
|
||||
model_name="payment",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("AYY", "AYY"),
|
||||
("cash", "Cash"),
|
||||
("bank_transfer", "Bank transfer"),
|
||||
],
|
||||
max_length=255,
|
||||
verbose_name="Source",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,13 +9,15 @@ import django.utils.timezone
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0010_auto_20170526_1910'),
|
||||
("members", "0010_auto_20170526_1910"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='submitted',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Submitted'),
|
||||
model_name="request",
|
||||
name="submitted",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="Submitted"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,16 +9,38 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0011_auto_20170526_2013'),
|
||||
("members", "0011_auto_20170526_2013"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MemberConflict',
|
||||
name="MemberConflict",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberconflict_first_member', to='members.Member')),
|
||||
('second_member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberconflict_second_member', to='members.Member')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_member",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="memberconflict_first_member",
|
||||
to="members.Member",
|
||||
),
|
||||
),
|
||||
(
|
||||
"second_member",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="memberconflict_second_member",
|
||||
to="members.Member",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,13 +9,19 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0012_memberconflict'),
|
||||
("members", "0012_memberconflict"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='member',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='payments', to='members.Member'),
|
||||
model_name="payment",
|
||||
name="member",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="payments",
|
||||
to="members.Member",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,18 +8,18 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0013_auto_20170601_1822'),
|
||||
("members", "0013_auto_20170601_1822"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, unique=True, verbose_name='Email'),
|
||||
model_name="member",
|
||||
name="email",
|
||||
field=models.EmailField(max_length=254, unique=True, verbose_name="Email"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='request',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, unique=True, verbose_name='Email'),
|
||||
model_name="request",
|
||||
name="email",
|
||||
field=models.EmailField(max_length=254, unique=True, verbose_name="Email"),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,19 +8,19 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0014_auto_20170920_1457'),
|
||||
("members", "0014_auto_20170920_1457"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='memberconflict',
|
||||
name='first_member',
|
||||
model_name="memberconflict",
|
||||
name="first_member",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='memberconflict',
|
||||
name='second_member',
|
||||
model_name="memberconflict",
|
||||
name="second_member",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='MemberConflict',
|
||||
name="MemberConflict",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,18 +9,22 @@ import django.utils.timezone
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0015_auto_20170925_1917'),
|
||||
("members", "0015_auto_20170925_1917"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='created',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Created'),
|
||||
model_name="member",
|
||||
name="created",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="Created"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='date',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date'),
|
||||
model_name="payment",
|
||||
name="date",
|
||||
field=models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="Date"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,12 +8,16 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0016_auto_20170925_1924'),
|
||||
("members", "0016_auto_20170925_1924"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='member',
|
||||
options={'permissions': (('check_by_email', 'Can check if user exists by email'),)},
|
||||
name="member",
|
||||
options={
|
||||
"permissions": (
|
||||
("check_by_email", "Can check if user exists by email"),
|
||||
)
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,20 +8,29 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0017_auto_20170926_1316'),
|
||||
("members", "0017_auto_20170926_1316"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='member',
|
||||
options={'permissions': (('check_by_email', 'Can check if user exists by email'), ('read_member', 'Can see member in list'))},
|
||||
name="member",
|
||||
options={
|
||||
"permissions": (
|
||||
("check_by_email", "Can check if user exists by email"),
|
||||
("read_member", "Can see member in list"),
|
||||
)
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='payment',
|
||||
options={'permissions': (('read_payment', 'Can see payment in list'),)},
|
||||
name="payment",
|
||||
options={"permissions": (("read_payment", "Can see payment in list"),)},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='request',
|
||||
options={'permissions': (('read_application', 'Can see member application in list'),)},
|
||||
name="request",
|
||||
options={
|
||||
"permissions": (
|
||||
("read_application", "Can see member application in list"),
|
||||
)
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,12 +8,19 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('members', '0018_auto_20170927_1918'),
|
||||
("members", "0018_auto_20170927_1918"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='member',
|
||||
options={'permissions': (('check_by_email', 'Can check if user exists by email'), ('read_member', 'Can see member in list')), 'verbose_name': 'Member', 'verbose_name_plural': 'Members'},
|
||||
name="member",
|
||||
options={
|
||||
"permissions": (
|
||||
("check_by_email", "Can check if user exists by email"),
|
||||
("read_member", "Can see member in list"),
|
||||
),
|
||||
"verbose_name": "Member",
|
||||
"verbose_name_plural": "Members",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.14 on 2022-08-01 19:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("members", "0019_auto_20171029_1143"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="payment",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.14 on 2022-08-01 19:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("members", "0020_alter_payment_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="member",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="request",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
+39
-31
@@ -2,18 +2,20 @@
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import Q, OuterRef, Subquery
|
||||
|
||||
|
||||
class BaseMember(models.Model):
|
||||
"""Abstract base model for member."""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
first_name = models.CharField(_("First name"), max_length=127)
|
||||
last_name = models.CharField(_("Last name"), max_length=127)
|
||||
email = models.EmailField(_("Email"), unique=True)
|
||||
POR = models.CharField(_("Place of residence"),
|
||||
max_length=255) # place of residence
|
||||
POR = models.CharField(
|
||||
_("Place of residence"), max_length=255
|
||||
) # place of residence
|
||||
AYY = models.BooleanField(_("AYY"), default=False)
|
||||
jas = models.BooleanField(_("JAS"), default=False)
|
||||
|
||||
@@ -34,7 +36,7 @@ class BaseMember(models.Model):
|
||||
self.email,
|
||||
self.POR,
|
||||
int(self.AYY),
|
||||
int(self.jas)
|
||||
int(self.jas),
|
||||
]
|
||||
|
||||
|
||||
@@ -42,11 +44,9 @@ class Request(BaseMember):
|
||||
"""Member request model represents one member request."""
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('read_application', 'Can see member application in list'),
|
||||
)
|
||||
permissions = (("read_application", "Can see member application in list"),)
|
||||
|
||||
submitted = models.DateTimeField(_('Submitted'), default=timezone.now)
|
||||
submitted = models.DateTimeField(_("Submitted"), default=timezone.now)
|
||||
|
||||
def to_member(self):
|
||||
"""Convert array to member model."""
|
||||
@@ -59,47 +59,55 @@ class Payment(models.Model):
|
||||
"""Payment model representing one payment event."""
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('read_payment', 'Can see payment in list'),
|
||||
)
|
||||
permissions = (("read_payment", "Can see payment in list"),)
|
||||
|
||||
date = models.DateTimeField(_('Date'), default=timezone.now)
|
||||
source = models.CharField(_('Source'), choices=[
|
||||
('AYY', _('AYY')),
|
||||
('cash', _('Cash')),
|
||||
('bank_transfer', _('Bank transfer')),
|
||||
], max_length=255)
|
||||
id = models.AutoField(primary_key=True)
|
||||
date = models.DateTimeField(_("Date"), default=timezone.now)
|
||||
source = models.CharField(
|
||||
_("Source"),
|
||||
choices=[
|
||||
("AYY", _("AYY")),
|
||||
("cash", _("Cash")),
|
||||
("bank_transfer", _("Bank transfer")),
|
||||
],
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
member = models.ForeignKey('Member',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='payments')
|
||||
member = models.ForeignKey(
|
||||
"Member",
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="payments",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""Return payment id and date."""
|
||||
return 'Payment no. {}, {}'.format(self.id, str(self.date))
|
||||
return "Payment no. {}, {}".format(self.id, str(self.date))
|
||||
|
||||
@staticmethod
|
||||
def find_payments_by_name(query_name):
|
||||
qs = Payment.objects.all()
|
||||
for term in query_name.split():
|
||||
qs = qs.filter(Q(member__first_name__icontains=term) | Q(member__last_name__icontains=term))
|
||||
qs = qs.filter(
|
||||
Q(member__first_name__icontains=term)
|
||||
| Q(member__last_name__icontains=term)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class Member(BaseMember):
|
||||
"""Member model represets one member on the registry."""
|
||||
|
||||
created = models.DateTimeField(_('Created'), default=timezone.now)
|
||||
created = models.DateTimeField(_("Created"), default=timezone.now)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('check_by_email', 'Can check if user exists by email'),
|
||||
('read_member', 'Can see member in list'),
|
||||
("check_by_email", "Can check if user exists by email"),
|
||||
("read_member", "Can see member in list"),
|
||||
)
|
||||
verbose_name = _('Member')
|
||||
verbose_name_plural = _('Members')
|
||||
verbose_name = _("Member")
|
||||
verbose_name_plural = _("Members")
|
||||
|
||||
@staticmethod
|
||||
def from_array(array):
|
||||
@@ -126,5 +134,5 @@ class Member(BaseMember):
|
||||
@staticmethod
|
||||
def get_members_with_latest_payment(members_query):
|
||||
"""Return QuerySet of given members QS with last_paid attribute."""
|
||||
latest = Payment.objects.filter(member=OuterRef('pk')).order_by('-date')
|
||||
return members_query.annotate(last_paid=Subquery(latest.values('date')[:1]))
|
||||
latest = Payment.objects.filter(member=OuterRef("pk")).order_by("-date")
|
||||
return members_query.annotate(last_paid=Subquery(latest.values("date")[:1]))
|
||||
|
||||
@@ -5,4 +5,4 @@ import logging
|
||||
|
||||
class CheckByEmailPermission(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return request.user.has_perm('members.check_by_email')
|
||||
return request.user.has_perm("members.check_by_email")
|
||||
|
||||
@@ -6,7 +6,7 @@ from .models import Member, Payment, Request
|
||||
class MemberResource(resources.ModelResource):
|
||||
class Meta:
|
||||
model = Member
|
||||
exclude = ['id', 'created']
|
||||
exclude = ["id", "created"]
|
||||
|
||||
|
||||
class PaymentResource(resources.ModelResource):
|
||||
@@ -14,13 +14,13 @@ class PaymentResource(resources.ModelResource):
|
||||
|
||||
class Meta:
|
||||
model = Payment
|
||||
exclude = ['id']
|
||||
exclude = ["id"]
|
||||
|
||||
def dehydrate_member(self, payment):
|
||||
return '{} {}'.format(payment.member.first_name, payment.member.last_name)
|
||||
return "{} {}".format(payment.member.first_name, payment.member.last_name)
|
||||
|
||||
|
||||
class ApplicationResource(resources.ModelResource):
|
||||
class Meta:
|
||||
model = Request
|
||||
exclude = ['id']
|
||||
exclude = ["id"]
|
||||
|
||||
+12
-3
@@ -7,11 +7,20 @@ from members.models import Member
|
||||
class MemberSerializer(serializers.ModelSerializer):
|
||||
"""Model serializer for member."""
|
||||
|
||||
paid = serializers.DateTimeField(source='last_paid')
|
||||
paid = serializers.DateTimeField(source="last_paid")
|
||||
|
||||
class Meta:
|
||||
"""Meta of member serializer."""
|
||||
|
||||
model = Member
|
||||
fields = ('id', 'first_name', 'last_name', 'email',
|
||||
'POR', 'AYY', 'jas', 'created', 'paid')
|
||||
fields = (
|
||||
"id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"POR",
|
||||
"AYY",
|
||||
"jas",
|
||||
"created",
|
||||
"paid",
|
||||
)
|
||||
|
||||
+33
-14
@@ -1,7 +1,7 @@
|
||||
"""File containing member application django tables."""
|
||||
|
||||
import django_tables2 as tables
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import F, OuterRef, Subquery
|
||||
from django.utils import timezone
|
||||
@@ -12,11 +12,16 @@ from members.models import Member, Payment, Request
|
||||
class MemberTable(tables.Table):
|
||||
"""Table for member."""
|
||||
|
||||
last_paid = tables.DateTimeColumn(verbose_name=_('Last paid'))
|
||||
last_paid = tables.DateTimeColumn(verbose_name=_("Last paid"))
|
||||
|
||||
options = tables.TemplateColumn(
|
||||
('<a class="data-table-button btn btn-primary" '
|
||||
'href="/members/edit/{{ record.id }}">') + _('Edit') + '</a>', verbose_name=""
|
||||
(
|
||||
'<a class="data-table-button btn btn-primary" '
|
||||
'href="/members/edit/{{ record.id }}">'
|
||||
)
|
||||
+ _("Edit")
|
||||
+ "</a>",
|
||||
verbose_name="",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -26,24 +31,34 @@ class MemberTable(tables.Table):
|
||||
|
||||
def render_last_paid(self, record):
|
||||
try:
|
||||
return timezone.localtime(record.payments.filter(member=record).latest('date').date).strftime('%-d.%-m.%Y %H:%M')
|
||||
return timezone.localtime(
|
||||
record.payments.filter(member=record).latest("date").date
|
||||
).strftime("%-d.%-m.%Y %H:%M")
|
||||
except ObjectDoesNotExist:
|
||||
return timezone.localtime(record.created).strftime('%-d.%-m.%Y %H:%M') + _(" (not paid)")
|
||||
return timezone.localtime(record.created).strftime("%-d.%-m.%Y %H:%M") + _(
|
||||
" (not paid)"
|
||||
)
|
||||
|
||||
def order_last_paid(self, queryset, is_descending):
|
||||
queryset = Member.get_members_with_latest_payment(queryset).order_by(('-' if is_descending else '') + 'last_paid')
|
||||
queryset = Member.get_members_with_latest_payment(queryset).order_by(
|
||||
("-" if is_descending else "") + "last_paid"
|
||||
)
|
||||
return (queryset, True)
|
||||
|
||||
|
||||
class PaymentTable(tables.Table):
|
||||
"""Table for payments."""
|
||||
|
||||
member = tables.Column(accessor='member', verbose_name=_('Member'))
|
||||
member = tables.Column(accessor="member", verbose_name=_("Member"))
|
||||
|
||||
options = tables.TemplateColumn(
|
||||
('<a class="data-table-button btn btn-primary" '
|
||||
'href="/members/edit_payment/{{ record.id }}">') + _('Edit') + '</a>',
|
||||
verbose_name=""
|
||||
(
|
||||
'<a class="data-table-button btn btn-primary" '
|
||||
'href="/members/edit_payment/{{ record.id }}">'
|
||||
)
|
||||
+ _("Edit")
|
||||
+ "</a>",
|
||||
verbose_name="",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -56,9 +71,13 @@ class RequestTable(tables.Table):
|
||||
"""Table for member applications."""
|
||||
|
||||
options = tables.TemplateColumn(
|
||||
('<a class="data-table-button btn btn-primary" '
|
||||
'href="/members/edit_application/{{ record.id }}">') + _('Edit') + '</a>',
|
||||
verbose_name=""
|
||||
(
|
||||
'<a class="data-table-button btn btn-primary" '
|
||||
'href="/members/edit_application/{{ record.id }}">'
|
||||
)
|
||||
+ _("Edit")
|
||||
+ "</a>",
|
||||
verbose_name="",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% trans "Moi" %} {{ first_name }}!
|
||||
|
||||
{% trans "Onnittelut! Sinut on hyväksytty Sähköinsinöörikillan jäseneksi." %}
|
||||
|
||||
{% trans "Käy kurkkaamassa killan nettisivuilta" %} (https://sik.ayy.fi) {% trans "tulevia tapahtumia ja piipahda kiltahuoneella tutustumassa uusiin kiltatovereihisi!" %}
|
||||
|
||||
{% trans "Liity myös killan TG-kanaville" %}:
|
||||
{% trans "SIK" %}: https://t.me/joinchat/A6EViD5FCWLxPcXCggY7hw
|
||||
{% trans "SIK-fuksit 2019" %}: http://tinyurl.com/sikfuksit19-tg
|
||||
{% trans "SIK-fuksit 2019 -tiedotuskanava" %}: http://tinyurl.com/sikfuksit19-tiedotus
|
||||
+84
-47
@@ -5,6 +5,7 @@ from unittest import skip
|
||||
from django.contrib.auth.models import User
|
||||
from members.models import Member, Payment, Request
|
||||
from rest_framework.authtoken.models import Token
|
||||
from datetime import timezone
|
||||
|
||||
|
||||
import logging
|
||||
@@ -17,16 +18,23 @@ class MemberRegisterTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Setup testing environment by creating member and admin."""
|
||||
memb = Member.objects.create(first_name="Tidus", last_name="Tester", email="tidus@tester.fi")
|
||||
payment = Payment.objects.create(member=memb, source='AYY')
|
||||
memb = Member.objects.create(
|
||||
first_name="Tidus", last_name="Tester", email="tidus@tester.fi"
|
||||
)
|
||||
payment = Payment.objects.create(member=memb, source="AYY")
|
||||
appl = Request.objects.create(
|
||||
first_name="Liisa", last_name="Mattila",
|
||||
email="liisa.mattila@pylly.com", POR="Kouvola",
|
||||
AYY=True, jas=False)
|
||||
first_name="Liisa",
|
||||
last_name="Mattila",
|
||||
email="liisa.mattila@pylly.com",
|
||||
POR="Kouvola",
|
||||
AYY=True,
|
||||
jas=False,
|
||||
)
|
||||
|
||||
username, password = 'test_admin', 'password123'
|
||||
username, password = "test_admin", "password123"
|
||||
test_admin = User.objects.create_superuser(
|
||||
username, 'myemail@test.com', password)
|
||||
username, "myemail@test.com", password
|
||||
)
|
||||
self.c = Client()
|
||||
self.c.login(username=username, password=password)
|
||||
|
||||
@@ -39,84 +47,113 @@ class MemberRegisterTestCase(TestCase):
|
||||
"""Test csv import only with single line in csv file."""
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(os.path.join(current_dir, 'test_resources', 'single_line_import.csv')) as csvFile:
|
||||
response = self.c.post('/members/import_csv', {
|
||||
'csvFile': csvFile,
|
||||
'delimiter': ';',
|
||||
'payment_source': 'AYY'
|
||||
}, follow=True)
|
||||
with open(
|
||||
os.path.join(current_dir, "test_resources", "single_line_import.csv")
|
||||
) as csvFile:
|
||||
response = self.c.post(
|
||||
"/members/import_csv",
|
||||
{"csvFile": csvFile, "delimiter": ";", "payment_source": "AYY"},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_import_csv_multi_line(self):
|
||||
"""Test csv import with multilined csv."""
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(os.path.join(current_dir, 'test_resources', 'multi_line_import.csv')) as csvFile:
|
||||
response = self.c.post('/members/import_csv', {
|
||||
'csvFile': csvFile,
|
||||
'delimiter': ';',
|
||||
'payment_source': 'AYY'
|
||||
}, follow=True)
|
||||
with open(
|
||||
os.path.join(current_dir, "test_resources", "multi_line_import.csv")
|
||||
) as csvFile:
|
||||
response = self.c.post(
|
||||
"/members/import_csv",
|
||||
{"csvFile": csvFile, "delimiter": ";", "payment_source": "AYY"},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_autocomplete_search_found(self):
|
||||
"""Test member autocomplete search"""
|
||||
search_terms = 'Tidus'
|
||||
response = self.c.get('/members/member-autocomplete?q={}'.format(search_terms), follow=True)
|
||||
results = response.json()['results']
|
||||
search_terms = "Tidus"
|
||||
response = self.c.get(
|
||||
"/members/member-autocomplete?q={}".format(search_terms), follow=True
|
||||
)
|
||||
results = response.json()["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
|
||||
def test_autocomplete_search_not_found(self):
|
||||
"""Test member autocomplete search"""
|
||||
search_terms = 'Notfound'
|
||||
response = self.c.get('/members/member-autocomplete?q={}'.format(search_terms), follow=True)
|
||||
results = response.json()['results']
|
||||
search_terms = "Notfound"
|
||||
response = self.c.get(
|
||||
"/members/member-autocomplete?q={}".format(search_terms), follow=True
|
||||
)
|
||||
results = response.json()["results"]
|
||||
self.assertEqual(len(results), 0)
|
||||
|
||||
def test_export_members_excel(self):
|
||||
"""Test if the user can download an excel file of the member register"""
|
||||
resp = self.c.get('/members/export_members')
|
||||
content_type = 'application/vnd.ms-excel'
|
||||
self.assertIn(content_type, resp['Content-Type'])
|
||||
resp = self.c.get("/members/export_members")
|
||||
content_type = "application/vnd.ms-excel"
|
||||
self.assertIn(content_type, resp["Content-Type"])
|
||||
|
||||
content = resp.content
|
||||
arrays = pyexcel.get_array(file_content=content, file_type='xlsx')
|
||||
tidus_array = ['Tidus', 'Tester', 'tidus@tester.fi', '', '0', '0']
|
||||
arrays = pyexcel.get_array(file_content=content, file_type="xlsx")
|
||||
tidus_array = ["Tidus", "Tester", "tidus@tester.fi", "", "0", "0"]
|
||||
self.assertIn(tidus_array, arrays)
|
||||
|
||||
def test_export_payments_excel(self):
|
||||
"""Test if the user can download an excel file of the payment register"""
|
||||
resp = self.c.get('/members/export_payments')
|
||||
content_type = 'application/vnd.ms-excel'
|
||||
self.assertIn(content_type, resp['Content-Type'])
|
||||
resp = self.c.get("/members/export_payments")
|
||||
content_type = "application/vnd.ms-excel"
|
||||
self.assertIn(content_type, resp["Content-Type"])
|
||||
|
||||
content = resp.content
|
||||
arrays = pyexcel.get_array(file_content=content, file_type='xlsx')
|
||||
created = Payment.objects.get(member__email='tidus@tester.fi').date.strftime('%Y-%m-%d %H:%M:%S')
|
||||
tidus_array = ['Tidus Tester', created, 'AYY']
|
||||
arrays = pyexcel.get_array(file_content=content, file_type="xlsx")
|
||||
created = (
|
||||
Payment.objects.get(member__email="tidus@tester.fi")
|
||||
.date.replace(tzinfo=timezone.utc)
|
||||
.astimezone(tz=None)
|
||||
.strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
tidus_array = ["Tidus Tester", created, "AYY"]
|
||||
self.assertIn(tidus_array, arrays)
|
||||
|
||||
def test_export_applications_excel(self):
|
||||
"""Test if the user can download an excel file of the member application register"""
|
||||
resp = self.c.get('/members/export_applications')
|
||||
content_type = 'application/vnd.ms-excel'
|
||||
self.assertIn(content_type, resp['Content-Type'])
|
||||
resp = self.c.get("/members/export_applications")
|
||||
content_type = "application/vnd.ms-excel"
|
||||
self.assertIn(content_type, resp["Content-Type"])
|
||||
|
||||
content = resp.content
|
||||
arrays = pyexcel.get_array(file_content=content, file_type='xlsx')
|
||||
submitted = Request.objects.get(email='liisa.mattila@pylly.com').submitted.strftime('%Y-%m-%d %H:%M:%S')
|
||||
liisa_array = ['Liisa', 'Mattila', 'liisa.mattila@pylly.com', 'Kouvola', '1', '0', submitted]
|
||||
arrays = pyexcel.get_array(file_content=content, file_type="xlsx")
|
||||
submitted = (
|
||||
Request.objects.get(email="liisa.mattila@pylly.com")
|
||||
.submitted.replace(tzinfo=timezone.utc)
|
||||
.astimezone(tz=None)
|
||||
.strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
liisa_array = [
|
||||
"Liisa",
|
||||
"Mattila",
|
||||
"liisa.mattila@pylly.com",
|
||||
"Kouvola",
|
||||
"1",
|
||||
"0",
|
||||
submitted,
|
||||
]
|
||||
self.assertIn(liisa_array, arrays)
|
||||
|
||||
def test_submit_member_application(self):
|
||||
"""Test if submitting a member application works"""
|
||||
data = {
|
||||
'first_name': 'Seppo', 'last_name': 'Saastamoinen',
|
||||
'email': 'seppo@saastamoin.en', 'jas': 'on',
|
||||
'POR': 'Dipolin viinibaari'
|
||||
"first_name": "Seppo",
|
||||
"last_name": "Saastamoinen",
|
||||
"email": "seppo@saastamoin.en",
|
||||
"jas": "on",
|
||||
"POR": "Dipolin viinibaari",
|
||||
}
|
||||
resp = self.c.post('/members/submit_application', data=data)
|
||||
resp = self.c.post("/members/submit_application", data=data)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.assertTrue(Request.objects.filter(email='seppo@saastamoin.en').exists())
|
||||
self.assertTrue(Request.objects.filter(email="seppo@saastamoin.en").exists())
|
||||
|
||||
@@ -6,10 +6,10 @@ from rest_framework.throttling import UserRateThrottle
|
||||
class BurstRateThrottle(UserRateThrottle):
|
||||
"""Class for burst rate throttle."""
|
||||
|
||||
scope = 'burst'
|
||||
scope = "burst"
|
||||
|
||||
|
||||
class SustainedRateThrottle(UserRateThrottle):
|
||||
"""Class for sustained rate throttle."""
|
||||
|
||||
scope = 'sustained'
|
||||
scope = "sustained"
|
||||
|
||||
+36
-57
@@ -1,6 +1,6 @@
|
||||
"""File containing Member application URLs."""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
|
||||
@@ -41,86 +41,65 @@ from members.views import application_submit
|
||||
# from members.views import validateEmail, validate_success, validate_fail
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# landing page
|
||||
url(r'^$', member_list),
|
||||
url(r'^list$', member_list),
|
||||
|
||||
re_path(r"^$", member_list),
|
||||
re_path(r"^list$", member_list),
|
||||
# add member form view
|
||||
url(r'^add$', member_add),
|
||||
|
||||
re_path(r"^add$", member_add),
|
||||
# add many members view
|
||||
url(r'^add_many$', member_add_many),
|
||||
|
||||
re_path(r"^add_many$", member_add_many),
|
||||
# edit member information view
|
||||
url(r'^edit/(?P<index>\d+)$', member_edit),
|
||||
|
||||
re_path(r"^edit/(?P<index>\d+)$", member_edit),
|
||||
# delete confirmation view
|
||||
url(r'^delete_member_confirm/(?P<index>\d+)$', member_delete_confirm),
|
||||
|
||||
re_path(r"^delete_member_confirm/(?P<index>\d+)$", member_delete_confirm),
|
||||
# list all member applications
|
||||
url(r'^applications$', application_list),
|
||||
|
||||
re_path(r"^applications$", application_list),
|
||||
# edit member application
|
||||
url(r'^edit_application/(?P<index>\d+)$', application_edit),
|
||||
|
||||
re_path(r"^edit_application/(?P<index>\d+)$", application_edit),
|
||||
# post request targets
|
||||
url(r'^submit_member$', member_submit),
|
||||
url(r'^update_member$', member_update),
|
||||
url(r'^delete_member$', member_delete),
|
||||
url(r'^submit_payment$', payment_submit),
|
||||
url(r'^update_payment$', payment_update),
|
||||
url(r'^delete_payment$', payment_delete),
|
||||
url(r'^submit_application$', application_submit),
|
||||
url(r'^accept_application$', application_accept),
|
||||
url(r'^delete_application$', application_delete),
|
||||
|
||||
re_path(r"^submit_member$", member_submit),
|
||||
re_path(r"^update_member$", member_update),
|
||||
re_path(r"^delete_member$", member_delete),
|
||||
re_path(r"^submit_payment$", payment_submit),
|
||||
re_path(r"^update_payment$", payment_update),
|
||||
re_path(r"^delete_payment$", payment_delete),
|
||||
re_path(r"^submit_application$", application_submit),
|
||||
re_path(r"^accept_application$", application_accept),
|
||||
re_path(r"^delete_application$", application_delete),
|
||||
# the actual member application form
|
||||
url(r'^application/$', application_form),
|
||||
|
||||
re_path(r"^application/$", application_form),
|
||||
# delete confirmation view for applications
|
||||
url(r'^delete_application_confirm/(?P<index>\d+)$',
|
||||
application_delete_confirm),
|
||||
|
||||
re_path(r"^delete_application_confirm/(?P<index>\d+)$", application_delete_confirm),
|
||||
# list all payment events
|
||||
url(r'^payments$', payment_list),
|
||||
|
||||
re_path(r"^payments$", payment_list),
|
||||
# add payment event
|
||||
url(r'^add_payment$', payment_add),
|
||||
|
||||
re_path(r"^add_payment$", payment_add),
|
||||
# edit payment event
|
||||
url(r'^edit_payment/(?P<index>\d+)$', payment_edit),
|
||||
|
||||
re_path(r"^edit_payment/(?P<index>\d+)$", payment_edit),
|
||||
# delete confirmation view
|
||||
url(r'^delete_payment_confirm/(?P<index>\d+)$', payment_delete_confirm),
|
||||
|
||||
re_path(r"^delete_payment_confirm/(?P<index>\d+)$", payment_delete_confirm),
|
||||
# post endpoint for confirming multiple entries
|
||||
url(r'^add_many_confirm$', add_many_confirm),
|
||||
|
||||
re_path(r"^add_many_confirm$", add_many_confirm),
|
||||
# settings page
|
||||
url(r'^settings$', settings_page),
|
||||
|
||||
re_path(r"^settings$", settings_page),
|
||||
# send CSV member data by POST
|
||||
url(r'^import_csv', import_csv),
|
||||
|
||||
re_path(r"^import_csv", import_csv),
|
||||
# export members as excel file
|
||||
url(r'export_members', export_members_excel),
|
||||
url(r'export_payments', export_payments_excel),
|
||||
url(r'export_applications', export_applications_excel),
|
||||
|
||||
re_path(r"export_members", export_members_excel),
|
||||
re_path(r"export_payments", export_payments_excel),
|
||||
re_path(r"export_applications", export_applications_excel),
|
||||
# rest api url
|
||||
url(r'^api/members/(?P<pk>\d+)$', MemberDetail.as_view()),
|
||||
|
||||
re_path(r"^api/members/(?P<pk>\d+)$", MemberDetail.as_view()),
|
||||
# member select autocomplete view
|
||||
url(
|
||||
r'^member-autocomplete/$',
|
||||
re_path(
|
||||
r"^member-autocomplete/$",
|
||||
MemberAutoComplete.as_view(),
|
||||
name='member-autocomplete',
|
||||
name="member-autocomplete",
|
||||
),
|
||||
|
||||
url(r'^check', CheckByEmail.as_view())
|
||||
re_path(r"^check", CheckByEmail.as_view()),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""File containing Members application views."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.forms.models import model_to_dict
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
@@ -12,6 +12,7 @@ import logging
|
||||
import html
|
||||
|
||||
from webapp.utils import send_email
|
||||
from webapp.utils import add_to_mailinglist
|
||||
|
||||
from members.views.utils import *
|
||||
from members.tables import RequestTable
|
||||
@@ -21,52 +22,55 @@ from members.views import error_view
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('members.read_application', raise_exception=True)
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("members.read_application", raise_exception=True)
|
||||
def application_list(request, *args, **kwargs):
|
||||
"""List member applications not yet processed."""
|
||||
applications = Request.objects.all()
|
||||
application_count = len(applications)
|
||||
table = RequestTable(applications,
|
||||
request=request,
|
||||
exclude=['id'],
|
||||
attrs={'class': 'table table-bordered table-hover'})
|
||||
table = RequestTable(
|
||||
applications,
|
||||
request=request,
|
||||
exclude=["id"],
|
||||
attrs={"class": "table table-bordered table-hover"},
|
||||
)
|
||||
|
||||
table.paginate(page=request.GET.get('page', 1), per_page=25)
|
||||
table.paginate(page=request.GET.get("page", 1), per_page=25)
|
||||
table_html = convert_table_to_html(table, request)
|
||||
context = {
|
||||
'table': table_html,
|
||||
'application_count': application_count,
|
||||
'notification': request.GET.get('notification', None)
|
||||
"table": table_html,
|
||||
"application_count": application_count,
|
||||
"notification": request.GET.get("notification", None),
|
||||
}
|
||||
return render(request, 'application_list.html', context)
|
||||
return render(request, "members/application_list.html", context)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('members.change_request', raise_exception=True)
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("members.change_request", raise_exception=True)
|
||||
def application_edit(request, *args, **kwargs):
|
||||
"""Edit member request information."""
|
||||
i = kwargs.pop('index', None)
|
||||
i = kwargs.pop("index", None)
|
||||
if i is None:
|
||||
return error_view(request, _('No application id specified'))
|
||||
return error_view(request, _("No application id specified"))
|
||||
else:
|
||||
application = Request.objects.get(id=i)
|
||||
form = ApplicationForm(instance=application)
|
||||
return render(
|
||||
request,
|
||||
'application_edit.html',
|
||||
{'application_id': i, 'form': form})
|
||||
"members/application_edit.html",
|
||||
{"application_id": i, "form": form},
|
||||
)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["POST"])
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('members.add_member', raise_exception=True)
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("members.add_member", raise_exception=True)
|
||||
def application_accept(request, *args, **kwargs):
|
||||
"""Accept application."""
|
||||
id = request.POST.get('id', None)
|
||||
id = request.POST.get("id", None)
|
||||
if id is not None:
|
||||
application = Request.objects.get(id=id)
|
||||
else:
|
||||
@@ -78,9 +82,15 @@ def application_accept(request, *args, **kwargs):
|
||||
application = form.save()
|
||||
|
||||
if Member.objects.filter(email=application.email).exists():
|
||||
return error_view(request, _(
|
||||
'Email {} is already in use by a member. Application cannot be accepted.'
|
||||
).format(application.email))
|
||||
return error_view(
|
||||
request,
|
||||
_(
|
||||
"Email {} is already in use by a member. Application cannot be accepted."
|
||||
).format(application.email),
|
||||
)
|
||||
|
||||
if application.jas:
|
||||
add_to_mailinglist(application.email)
|
||||
|
||||
member = application.to_member()
|
||||
member.save()
|
||||
@@ -88,24 +98,25 @@ def application_accept(request, *args, **kwargs):
|
||||
|
||||
logging.info(
|
||||
"Accepted application in member "
|
||||
"register with the following info: {}"
|
||||
.format(form))
|
||||
notification = "{} {}.".format(_("Successfully accepted application"),
|
||||
str(application))
|
||||
"register with the following info: {}".format(form)
|
||||
)
|
||||
notification = "{} {}.".format(
|
||||
_("Successfully accepted application"), str(application)
|
||||
)
|
||||
|
||||
subject = _('Jäsenhakemuksesi Sähköinsinöörikiltaan on hyväksytty!')
|
||||
subject = _("Jäsenhakemuksesi Sähköinsinöörikiltaan on hyväksytty!")
|
||||
|
||||
message = render_to_string(
|
||||
'members:email_application_accept.html', {
|
||||
'first_name': application.first_name
|
||||
}
|
||||
"members/email_application_accept.html",
|
||||
{"first_name": application.first_name},
|
||||
)
|
||||
send_email(member.email, subject, message)
|
||||
send_email(member.email, subject, message, True)
|
||||
|
||||
return HttpResponseRedirect(
|
||||
'/members/list?notification={}'.format(html.escape(notification)))
|
||||
"/members/list?notification={}".format(html.escape(notification))
|
||||
)
|
||||
except Exception as ex:
|
||||
logging.exception('Exception while accepting application')
|
||||
logging.exception("Exception while accepting application")
|
||||
return error_view(request, str(ex))
|
||||
else:
|
||||
logging.info(form)
|
||||
@@ -114,52 +125,55 @@ def application_accept(request, *args, **kwargs):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["POST"])
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('members.delete_request', raise_exception=True)
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("members.delete_request", raise_exception=True)
|
||||
def application_delete(request, *args, **kwargs):
|
||||
"""Delete member application."""
|
||||
try:
|
||||
id = request.POST['id']
|
||||
id = request.POST["id"]
|
||||
except KeyError:
|
||||
return error_view(request, _('No application id specified'))
|
||||
return error_view(request, _("No application id specified"))
|
||||
|
||||
try:
|
||||
application = Request.objects.get(id=id)
|
||||
notification = "{} {}.".format(_("Successfully deleted application"),
|
||||
str(application))
|
||||
notification = "{} {}.".format(
|
||||
_("Successfully deleted application"), str(application)
|
||||
)
|
||||
application.delete()
|
||||
logging.info(
|
||||
"Delete application in member register with the following id: {}"
|
||||
.format(id))
|
||||
"Delete application in member register with the following id: {}".format(id)
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
'/members/applications?notification={}'
|
||||
.format(html.escape(notification)))
|
||||
"/members/applications?notification={}".format(html.escape(notification))
|
||||
)
|
||||
except:
|
||||
return error_view(request, _('Could not delete application object'))
|
||||
return error_view(request, _("Could not delete application object"))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('members.delete_request', raise_exception=True)
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("members.delete_request", raise_exception=True)
|
||||
def application_delete_confirm(request, *args, **kwargs):
|
||||
"""Confirm application deletion."""
|
||||
i = kwargs.pop('index', None)
|
||||
i = kwargs.pop("index", None)
|
||||
if i is None:
|
||||
return error_view(request, _('No application id specified'))
|
||||
return error_view(request, _("No application id specified"))
|
||||
else:
|
||||
application = Request.objects.get(id=i)
|
||||
form = ApplicationForm(instance=application)
|
||||
return render(request,
|
||||
'application_delete_confirm.html',
|
||||
{'application_id': i, 'form': form})
|
||||
return render(
|
||||
request,
|
||||
"members/application_delete_confirm.html",
|
||||
{"application_id": i, "form": form},
|
||||
)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def application_form(request, *args, **kwargs):
|
||||
"""Render member application form."""
|
||||
form = ApplicationForm()
|
||||
return render(request, 'application_index.html', {'form': form})
|
||||
return render(request, "members/application_index.html", {"form": form})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -171,19 +185,22 @@ def application_submit(request, *args, **kwargs):
|
||||
form.save()
|
||||
try:
|
||||
application = form.instance
|
||||
email = form.cleaned_data.get('email', '')
|
||||
email = form.cleaned_data.get("email", "")
|
||||
|
||||
subject = _('Jäsenhakemuksesi Sähköinsinöörikiltaan on lähetetty onnistuneesti!')
|
||||
subject = _(
|
||||
"Jäsenhakemuksesi Sähköinsinöörikiltaan on lähetetty onnistuneesti!"
|
||||
)
|
||||
|
||||
message = render_to_string(
|
||||
'members:email_application_submit.html', {
|
||||
'application': application,
|
||||
'ayy': _('Kyllä') if application.AYY else _('Ei'),
|
||||
'jas': _('Kyllä') if application.jas else _('Ei')
|
||||
}
|
||||
"members/email_application_submit.html",
|
||||
{
|
||||
"application": application,
|
||||
"ayy": _("Kyllä") if application.AYY else _("Ei"),
|
||||
"jas": _("Kyllä") if application.jas else _("Ei"),
|
||||
},
|
||||
)
|
||||
send_email(email, subject, message)
|
||||
finally:
|
||||
return render(request, 'application_success.html', {})
|
||||
return render(request, "members/application_success.html", {})
|
||||
else:
|
||||
return error_view(request, form.errors)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user