Compare commits

...

32 Commits

Author SHA1 Message Date
fa8e30b299 Merge pull request 'Authorisierung' (#22) from feature-auth into main
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Reviewed-on: #22
2021-06-28 18:12:29 +02:00
21067d059b optimise drone build
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2021-06-27 16:51:48 +02:00
c8c4d9f99c add docker publish
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-27 16:39:42 +02:00
78c539c30a implement auth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-27 14:25:44 +02:00
5113f6995e implement languages update
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-27 13:51:40 +02:00
15b71459ee implement updating skills 2021-06-27 13:38:16 +02:00
68b84f50ca implement contacts update 2021-06-27 13:07:54 +02:00
54a6686474 implement address update 2021-06-27 12:20:36 +02:00
101bc20923 extract update profile handler handler 2021-06-27 11:55:08 +02:00
3bed7222ec Merge pull request 'add pre-commit hook for yapf auto formatting (#20)' (#21) from LukasGrossberger/ki-backend:yapf-pre-commit into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #21
2021-06-27 11:42:29 +02:00
cb97db9579 add pre-commit hook for yapf auto formatting (#20)
All checks were successful
continuous-integration/drone/pr Build is passing
2021-06-27 10:57:50 +02:00
d10706e301 Merge pull request 'Komplettes Profil zurückgeben' (#19) from feature-return-entire-profile into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #19
2021-06-27 09:44:59 +02:00
fc01bec163 return full profile response
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-26 12:16:14 +02:00
3bd9b03002 add yapf config file 2021-06-26 11:40:29 +02:00
ace1b1ed85 seed entire profile 2021-06-26 10:51:39 +02:00
09669cf369 update readme
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-26 09:49:09 +02:00
5dc0f7153d Merge pull request 'Behebung Fehler in 1:n Verknüpfung Benutzer ↔ Token' (#17) from fix-login into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #17
2021-06-23 12:45:44 +02:00
5d259635a2 fix db error on second login
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-22 17:52:22 +02:00
b09b072261 Merge remote-tracking branch 'remotes/origin/feature-cors'
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-22 09:23:34 +02:00
7e8c5a7de0 Merge pull request 'pre-commit mit flake8' (#14) from feature-pre-commit into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #14
2021-06-22 09:13:06 +02:00
a9f9c36eda add cors
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-21 22:21:25 +02:00
a349eff9b0 only build prs and main
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-21 22:00:33 +02:00
4d88ee8b77 add pre-commit
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-21 21:45:58 +02:00
9e48953fc3 Merge pull request 'Logging' (#7) from feature-logging into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #7
2021-06-21 21:44:33 +02:00
d96dfa8800 fix code style
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-21 21:22:39 +02:00
2f0dd2ab9f users/login: Provider user_id together with token
Some checks failed
continuous-integration/drone/push Build is failing
2021-06-21 18:41:35 +02:00
ea7b6391c1 Merge pull request 'Add yapf to Pipenv environment' (#6) from add-yapf into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #6
2021-06-21 17:38:05 +02:00
3dcba71a6d add logging
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-21 17:35:28 +02:00
cbf3002b93 Reformat source code
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-06-21 17:28:57 +02:00
59de00527d Ignore *.pyc-files for git
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-21 16:12:54 +02:00
6d4f933585 Add yapf to Pipfile 2021-06-21 16:09:37 +02:00
1390dfa8e6 Add usage of yapf to README 2021-06-21 16:09:26 +02:00
26 changed files with 770 additions and 202 deletions

View File

@ -5,10 +5,26 @@ name: default
steps:
- name: qa
image: python:3.8-alpine
image: registry.wtf-eg.net/ki-backend-builder:1.0.0
commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv install --dev
- pipenv run flake8
- pipenv run python -m unittest discover ki
- name: docker-publish
image: plugins/docker
settings:
registry: registry.wtf-eg.net
repo: registry.wtf-eg.net/ki-backend
target: ki-backend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
when:
branch:
- main
image_pull_secrets:
- dockerconfig

View File

@ -1,5 +1,4 @@
[flake8]
max-line-length = 120
exclude =
.git,
extend-exclude =
migrations

1
.gitignore vendored
View File

@ -1 +1,2 @@
/.env
*.pyc

20
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,20 @@
- repo: local
hooks:
- id: flake8
name: flake8
entry: flake8
language: system
files: ^.*\.py$
exclude: ^(migrations).*$
- id: yapf
name: yapf
entry: yapf -i
language: system
files: ^.*\.py$
exclude: ^(migrations).*$
- id: unittest
name: unittest
entry: python -m unittest discover ki
language: system
exclude: .*
always_run: true

4
.style.yapf Normal file
View File

@ -0,0 +1,4 @@
[style]
based_on_style = pep8
allow_split_before_dict_value = 0
column_limit = 120

View File

@ -1,17 +1,4 @@
FROM python:3.8-alpine as base
ENV PYROOT /pyroot
ENV PYTHONUSERBASE $PYROOT
FROM base as builder
RUN apk add --no-cache \
gcc \
g++ \
musl-dev \
python3-dev && \
pip3 install pipenv
FROM registry.wtf-eg.net/ki-backend-builder:1.0.0 as builder
COPY Pipfile* ./
@ -19,7 +6,7 @@ RUN PIP_USER=1 PIP_IGNORE_INSTALLED=1 pipenv install --system --deploy --ignore-
RUN pip3 uninstall --yes pipenv
FROM base
FROM registry.wtf-eg.net/ki-backend-base:1.0.0 as ki-backend
# Install six explicitly. Otherwise Python complains about it missing.
RUN pip3 install six

View File

@ -11,9 +11,12 @@ flask-sqlalchemy = "~=2.5.1"
sqlalchemy = "~=1.4.18"
waitress = "~=2.0.0"
pyyaml = "~=5.4.1"
flask-cors = "~=3.0.10"
[dev-packages]
flake8 = "~=3.9.2"
yapf = "~=0.31.0"
pre-commit = "~=2.13.0"
[requires]
python_version = "3.8"

135
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "439b60cb87b0180f0b78c531085f9bbeef7685ef038256f80b0a8123e7d144e6"
"sha256": "11a821c6c1f072dcf7c39a020056fa289b7a5283aa33d96e2ed6860fbc023fa4"
},
"pipfile-spec": 6,
"requires": {
@ -21,7 +21,6 @@
"sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51",
"sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.6.5"
},
"click": {
@ -29,7 +28,6 @@
"sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
"sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.1"
},
"flask": {
@ -40,6 +38,14 @@
"index": "pypi",
"version": "==2.0.1"
},
"flask-cors": {
"hashes": [
"sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438",
"sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"
],
"index": "pypi",
"version": "==3.0.10"
},
"flask-migrate": {
"hashes": [
"sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702",
@ -116,7 +122,6 @@
"sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c",
"sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"jinja2": {
@ -124,7 +129,6 @@
"sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
"sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.1"
},
"mako": {
@ -132,7 +136,6 @@
"sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab",
"sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.4"
},
"markupsafe": {
@ -172,7 +175,6 @@
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"python-dateutil": {
@ -180,7 +182,6 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1"
},
"python-dotenv": {
@ -241,7 +242,6 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlalchemy": {
@ -293,11 +293,38 @@
"sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42",
"sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"version": "==1.4.4"
},
"cfgv": {
"hashes": [
"sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1",
"sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"
],
"version": "==3.3.0"
},
"distlib": {
"hashes": [
"sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736",
"sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"
],
"version": "==0.3.2"
},
"filelock": {
"hashes": [
"sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
"sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
],
"version": "==3.0.12"
},
"flake8": {
"hashes": [
"sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b",
@ -306,6 +333,13 @@
"index": "pypi",
"version": "==3.9.2"
},
"identify": {
"hashes": [
"sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421",
"sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"
],
"version": "==2.2.10"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@ -313,12 +347,26 @@
],
"version": "==0.6.1"
},
"nodeenv": {
"hashes": [
"sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b",
"sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"
],
"version": "==1.6.0"
},
"pre-commit": {
"hashes": [
"sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378",
"sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"
],
"index": "pypi",
"version": "==2.13.0"
},
"pycodestyle": {
"hashes": [
"sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
"sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.7.0"
},
"pyflakes": {
@ -326,8 +374,71 @@
"sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
"sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.3.1"
},
"pyyaml": {
"hashes": [
"sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
"sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
"sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
"sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
"sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
"sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
"sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
"sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
"sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
"sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
"sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
"sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
"sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
"sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
"sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
"sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
"sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
"sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
"sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
"sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
"sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
"sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
"sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
"sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
"sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
],
"index": "pypi",
"version": "==5.4.1"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"version": "==1.16.0"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"version": "==0.10.2"
},
"virtualenv": {
"hashes": [
"sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467",
"sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"
],
"version": "==20.4.7"
},
"yapf": {
"hashes": [
"sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d",
"sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e"
],
"index": "pypi",
"version": "==0.31.0"
}
}
}

View File

@ -9,6 +9,7 @@
- Python 3.8
- [Pipenv](https://github.com/pypa/pipenv)
### Entwicklungsumgebung aufbauen und starten
Ggf. vorher aufräumen
@ -21,25 +22,39 @@ rm data/ki.sqlite
cp env.dev .env
pipenv install --dev
pipenv shell
export FLASK_APP=app.py
flask db upgrade
flask seed
flask seed --dev
flask run
```
http://localhost:5000/
### Tests ausführen
### pre-commit einrichten
Damit mensch nicht verpeilt kaputten Code Style zu commiten,
kann pre-commit benutzt werden. Einmal im Virtualenv ausführen:
```
pre-commit install
```
### `alembic` Befehle
`alembic` ist über [Flask-Migrate](https://flask-migrate.readthedocs.io/en/latest/index.html) eingebunden.
Es wird über `flask db ...` aufgerufen.
### QA
```
python -m unittest discover ki
```
# Code formatieren
yapf -i --recursive ki/
### Linting
```
# Code-Style prüfen
flake8
```

14
app.py
View File

@ -1,17 +1,27 @@
import logging
import os
from dotenv import load_dotenv, find_dotenv
from flask import Flask
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
load_dotenv(find_dotenv())
loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING)
loglevel = int(loglevel)
logging.basicConfig(level=loglevel)
logging.debug("Hello from KI")
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["KI_DATA_DIR"] = os.path.dirname(__file__) + "/data"
app.config["KI_AUTH"] = os.getenv("KI_AUTH")
app.config["CORS_ORIGINS"] = os.getenv("CORS_ORIGINS", "*")
CORS(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

View File

@ -1,3 +1,5 @@
---
peter:
password: geheim
klaus:
password: jutta

View File

@ -3,3 +3,11 @@ id,name
2,Vue.js
3,Python
4,JavaScript
5,Angular
6,Flask
7,SQLAlchemy
8,Rust
9,MySQL
10,PostgreSQL
11,SQLite
12,Node.js

1 id name
3 2 Vue.js
4 3 Python
5 4 JavaScript
6 5 Angular
7 6 Flask
8 7 SQLAlchemy
9 8 Rust
10 9 MySQL
11 10 PostgreSQL
12 11 SQLite
13 12 Node.js

View File

@ -1,14 +0,0 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: qa
image: python3.8-alpine
commands:
- apk add --no-cache gcc g++ musl-dev python3-dev
- pip3 install pipenv
- pipenv install --system
- flake8
- python -m unittest discover ki

10
env.dev
View File

@ -1,3 +1,11 @@
SQLALCHEMY_DATABASE_URI = 'sqlite:///data/ki.sqlite'
SQLALCHEMY_DATABASE_URI=sqlite:///data/ki.sqlite
CORS_ORIGINS=*
FLASK_APP=app.py
FLASK_ENV=development
KI_AUTH=file
# 10 = debug
KI_LOGLEVEL=10

1
ki/actions/__init__.py Normal file
View File

@ -0,0 +1 @@
from ki.actions.seed import seed # noqa

89
ki/actions/seed.py Normal file
View File

@ -0,0 +1,89 @@
import csv
import logging
from app import app, db
from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSkill, User
def seed(dev: bool):
skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv"
logging.info("importing skills")
with open(skill_seed_file_path) as skills_file:
skills_csv_reader = csv.DictReader(skills_file)
for skill in skills_csv_reader:
id = int(skill["id"])
db_skill = Skill.query.get(id)
if db_skill is None:
db.session.add(Skill(id=int(skill["id"]), name=skill["name"]))
logging.info("importing languages")
iso_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/iso_639_1.csv"
with open(iso_seed_file_path) as iso_file:
iso_csv_reader = csv.DictReader(iso_file)
for iso in iso_csv_reader:
id = iso["639-1"]
db_language = Language.query.get(id)
if db_language is None:
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
if dev:
logging.info("seeding peter :)")
peter = User(auth_id="peter")
db.session.add(peter)
peters_profile = Profile(nickname="peternichtlustig",
pronouns="Herr Dr. Dr.",
volunteerwork="Gartenverein",
freetext="Ich mag Kaffee",
user=peter)
db.session.add(peters_profile)
matrix_type = ContactType(name="Matrix")
db.session.add(matrix_type)
matrix_contact = Contact(profile=peters_profile, contacttype=matrix_type, content="@peter:wtf-eg.de")
db.session.add(matrix_contact)
email_type = ContactType(name="E-Mail")
db.session.add(email_type)
email_contact = Contact(profile=peters_profile, contacttype=email_type, content="peter@wtf-eg.de")
db.session.add(email_contact)
peters_address = Address(name="Peter Nichtlustig",
street="Waldweg",
house_number="23i",
additional="Hinterhaus",
postcode="13337",
city="Bielefeld",
country="Deutschland",
profile=peters_profile)
db.session.add(peters_address)
peters_python_skill = ProfileSkill(profile=peters_profile, skill_id=3, level=3)
db.session.add(peters_python_skill)
peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5)
db.session.add(peters_php_skill)
peter_de = ProfileLanguage(profile=peters_profile, language_id="de", level=5)
db.session.add(peter_de)
peter_fr = ProfileLanguage(profile=peters_profile, language_id="fr", level=3)
db.session.add(peter_fr)
logging.info("seeding klaus :D")
klaus = User(auth_id="klaus")
db.session.add(klaus)
db.session.commit()

View File

@ -1,39 +1,10 @@
import csv
import click
from ki.models import Language, Skill
from app import app, db
def seed():
skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv"
print("importing skills")
with open(skill_seed_file_path) as skills_file:
skills_csv_reader = csv.DictReader(skills_file)
for skill in skills_csv_reader:
id = int(skill["id"])
db_skill = Skill.query.get(id)
if db_skill is None:
db.session.add(Skill(id=int(skill["id"]), name=skill["name"]))
iso_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/iso_639_1.csv"
with open(iso_seed_file_path) as iso_file:
iso_csv_reader = csv.DictReader(iso_file)
for iso in iso_csv_reader:
id = iso["639-1"]
db_language = Language.query.get(id)
if db_language is None:
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
db.session.commit()
from app import app
from ki.actions import seed
@app.cli.command("seed")
def seed_command():
seed()
@click.option("--dev", is_flag=True)
def seed_command(dev):
seed(dev)

1
ki/handlers/__init__.py Normal file
View File

@ -0,0 +1 @@
from ki.handlers.update_profile import update_profile # noqa

View File

@ -0,0 +1,118 @@
from flask import make_response, request
from sqlalchemy import not_
from ki.models import Address, Contact, ContactType, Language, User, Profile, ProfileLanguage, ProfileSkill, Skill
from app import db
def update_address(profile, address_data):
address = profile.address
if (address is None):
address = Address(profile=profile)
db.session.add(address)
address.name = address_data.get("name", "")
address.street = address_data.get("street", "")
address.house_number = address_data.get("house_number", "")
address.additional = address_data.get("additional", "")
address.postcode = address_data.get("postcode", "")
address.city = address_data.get("city", "")
address.country = address_data.get("country", "")
def update_languages(profile, languages_data):
profile_language_ids = []
for language_data in languages_data:
language_id = language_data["language"]["id"]
language = Language.query.get(language_id)
profile_language = ProfileLanguage.query.filter(ProfileLanguage.profile == profile,
ProfileLanguage.language_id == language_id).first()
if profile_language is None:
profile_language = ProfileLanguage(profile=profile, language=language)
db.session.add(profile_language)
profile_language.level = language_data["level"]
profile_language_ids.append(language_id)
ProfileLanguage.query.filter(ProfileLanguage.profile == profile,
not_(ProfileLanguage.language_id.in_(profile_language_ids))).delete()
def update_skills(profile, skills_data):
profile_skill_ids = []
for skill_data in skills_data:
skill_name = skill_data["skill"]["name"]
skill = Skill.query.filter(Skill.name == skill_name).first()
if (skill is None):
skill = Skill(name=skill_name)
db.session.add(skill)
profile_skill = ProfileSkill.query.filter(ProfileSkill.profile == profile, ProfileSkill.skill == skill).first()
if (profile_skill is None):
profile_skill = ProfileSkill(profile=profile, skill=skill)
db.session.add(profile_skill)
profile_skill.level = skill_data["level"]
profile_skill_ids.append(skill.id)
ProfileSkill.query.filter(ProfileSkill.profile == profile,
not_(ProfileSkill.skill_id.in_(profile_skill_ids))).delete()
def update_contacts(profile, contacts_data):
contact_ids_to_be_deleted = list(map(lambda c: c.id, profile.contacts))
for contact_data in contacts_data:
contacttype_name = contact_data["contacttype"]["name"]
contacttype = ContactType.query.filter(ContactType.name == contacttype_name).first()
if (contacttype is None):
contacttype = ContactType(name=contacttype_name)
db.session.add(contacttype)
if "id" in contact_data:
contact_id = int(contact_data["id"])
contact_ids_to_be_deleted.remove(contact_id)
contact = Contact.query.get(contact_id)
else:
contact = Contact(profile=profile, contacttype=contacttype)
db.session.add(contact)
contact.contacttype_id = contacttype.id
contact.content = contact_data["content"]
Contact.query.filter(Contact.id.in_(contact_ids_to_be_deleted)).delete()
def update_profile(user_id: int):
user = User.query.get(user_id)
if user is None:
return make_response({}, 404)
profile = user.profile
if (profile is None):
profile = Profile(user=user, nickname=user.auth_id)
db.session.add(profile)
profile.pronouns = request.json.get("pronouns", "")
profile.volunteerwork = request.json.get("volunteerwork", "")
profile.freetext = request.json.get("freetext", "")
update_address(profile, request.json.get("address", {}))
update_contacts(profile, request.json.get("contacts", {}))
update_skills(profile, request.json.get("skills", {}))
update_languages(profile, request.json.get("languages", {}))
db.session.commit()
return make_response({"profile": profile.to_dict()})

View File

@ -13,7 +13,7 @@ class User(db.Model):
auth_id = Column(String(50), nullable=False, unique=True)
profile_id = Column(Integer, ForeignKey("profile.id"), nullable=True)
tokens = relationship("Token", uselist=False, back_populates="user")
tokens = relationship("Token", back_populates="user")
profile = relationship("Profile", back_populates="user")
def to_dict(self):
@ -29,10 +29,7 @@ class Profile(db.Model):
volunteerwork = Column(String(4000), default="")
freetext = Column(String(4000), default="")
created = Column(DateTime, nullable=False, default=datetime.now)
updated = Column(DateTime,
onupdate=datetime.now,
nullable=False,
default=datetime.now)
updated = Column(DateTime, onupdate=datetime.now, nullable=False, default=datetime.now)
user = relationship("User", back_populates="profile", uselist=False)
contacts = relationship("Contact")
@ -42,10 +39,15 @@ class Profile(db.Model):
def to_dict(self):
return {
"user_id": self.user.id,
"nickname": self.nickname,
"pronouns": self.pronouns,
"volunteerwork": self.volunteerwork,
"freetext": self.freetext
"freetext": self.freetext,
"address": self.address.to_dict(),
"contacts": list(map(lambda contact: contact.to_dict(), self.contacts)),
"skills": list(map(lambda skill: skill.to_dict(), self.skills)),
"languages": list(map(lambda language: language.to_dict(), self.languages))
}
@ -58,6 +60,9 @@ class Token(db.Model):
user = relationship("User", back_populates="tokens")
def to_dict(self):
return {"user_id": self.user_id, "token": self.token}
class Contact(db.Model):
__tablename__ = "contact"
@ -65,12 +70,18 @@ class Contact(db.Model):
id = Column(Integer, primary_key=True)
profile_id = Column(Integer, ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="contacts")
contacttype_id = Column(Integer,
ForeignKey("contacttype.id"),
nullable=False)
contacttype_id = Column(Integer, ForeignKey("contacttype.id"), nullable=False)
contacttype = relationship("ContactType")
content = Column(String(200), nullable=False)
def to_dict(self):
return {
"id": self.id,
"profile_id": self.profile_id,
"contacttype": self.contacttype.to_dict(),
"content": self.content
}
class ContactType(db.Model):
__tablename__ = "contacttype"
@ -78,6 +89,9 @@ class ContactType(db.Model):
id = Column(Integer, primary_key=True)
name = Column(String(25), nullable=False)
def to_dict(self):
return {"id": self.id, "name": self.name}
class Address(db.Model):
__tablename__ = "address"
@ -94,6 +108,19 @@ class Address(db.Model):
profile_id = Column(Integer, ForeignKey("profile.id"), nullable=False)
profile = relationship("Profile", back_populates="address")
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"street": self.street,
"house_number": self.house_number,
"additional": self.additional,
"postcode": self.postcode,
"city": self.city,
"country": self.country,
"profile_id": self.profile_id
}
class Skill(db.Model):
__tablename__ = "skill"
@ -104,7 +131,7 @@ class Skill(db.Model):
profiles = relationship("ProfileSkill", back_populates="skill")
def to_dict(self):
return {"id": self.id, "name": self.name}
return {"id": self.id, "name": self.name, "icon_url": "/skills/{}/icon".format(self.id)}
class ProfileSkill(db.Model):
@ -117,6 +144,9 @@ class ProfileSkill(db.Model):
profile = relationship("Profile", back_populates="skills")
skill = relationship("Skill", back_populates="profiles")
def to_dict(self):
return {"profile_id": self.profile_id, "skill": self.skill.to_dict(), "level": self.level}
class Language(db.Model):
__tablename__ = "language"
@ -127,7 +157,7 @@ class Language(db.Model):
profiles = relationship("ProfileLanguage", back_populates="language")
def to_dict(self):
return {"id": self.id, "name": self.name}
return {"id": self.id, "name": self.name, "icon_url": "/languages/{}/icon".format(self.id)}
class ProfileLanguage(db.Model):
@ -139,3 +169,6 @@ class ProfileLanguage(db.Model):
profile = relationship("Profile", back_populates="languages")
language = relationship("Language", back_populates="profiles")
def to_dict(self):
return {"profile_id": self.profile_id, "language": self.language.to_dict(), "level": self.level}

View File

@ -3,8 +3,9 @@ from flask import g, make_response, request, send_file
from functools import wraps
from ki.auth import auth
from ki.models import Language, Skill, Token, User, Profile
from app import app, db
from ki.handlers import update_profile as update_profile_handler
from ki.models import Language, Skill, Token, User
from app import app
def token_auth(func):
@ -99,7 +100,7 @@ def login():
if token is None:
return make_response({}, 403)
return make_response({"token": token.token})
return make_response({"token": token.token, "user_id": token.user_id})
@app.route("/users/<user_id>/profile")
@ -115,32 +116,22 @@ def get_user_profile(user_id):
if profile is None:
return make_response({}, 404)
return make_response({"profile": profile.to_dict()})
return make_response({
"profile": profile.to_dict(),
})
@app.route("/users/<user_id>/profile", methods=["POST"])
@token_auth
def update_profile(user_id):
user = User.query.filter(User.id == int(user_id)).first()
if g.user.id != int(user_id):
return make_response({}, 403)
if user is None:
return make_response({}, 404)
profile = user.profile
if (profile is None):
profile = Profile(user=user, nickname=user.auth_id)
db.session.add(profile)
profile.pronouns = request.json.get("pronouns", "")
profile.volunteerwork = request.json.get("volunteerwork", "")
profile.freetext = request.json.get("freetext", "")
db.session.commit()
return make_response(profile.to_dict(), 200)
return update_profile_handler(int(user_id))
@app.route("/skills")
@token_auth
def get_skills():
return handle_completion_request(Skill, "skills")
@ -152,6 +143,7 @@ def get_skill_icon(skill_id):
@app.route("/languages")
@token_auth
def get_languages():
return handle_completion_request(Language, "languages")

35
ki/test/ApiTest.py Normal file
View File

@ -0,0 +1,35 @@
from alembic import command
import json
import unittest
from app import app, db, migrate
from ki.actions import seed
class ApiTest(unittest.TestCase):
maxDiff = None
def setUp(self):
app.debug = True
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
self.client = app.test_client()
with app.app_context():
config = migrate.get_config()
command.upgrade(config, "head")
seed(True)
def tearDown(self):
db.drop_all()
db.engine.dispose()
def login(self, username, password):
login_data = {"username": username, "password": password}
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")
self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json)
return login_response.json

View File

@ -0,0 +1,26 @@
import json
import unittest
from ki.test.ApiTest import ApiTest
class TestLoginEndpoint(ApiTest):
def test_login(self):
response1_data = self.login("peter", "geheim")
response2_data = self.login("peter", "geheim")
self.assertNotEqual(response1_data["token"], response2_data["token"])
def test_login_wrong_credentails(self):
login_data = {"username": "peter", "password": "123456"}
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")
self.assertEqual(login_response.status_code, 403)
def test_login_unknown_user(self):
login_data = {"username": "karl", "password": "123456"}
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")
self.assertEqual(login_response.status_code, 403)
if __name__ == "main":
unittest.main()

View File

@ -1,95 +1,223 @@
from alembic import command
import unittest
import json
from app import app, db, migrate
from ki.commands import seed
from ki.models import Profile, User
from app import app
from ki.models import User
from ki.test.ApiTest import ApiTest
class TestProfileEndpoint(unittest.TestCase):
def setUp(self):
app.debug = True
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
self.client = app.test_client()
class TestProfileEndpoint(ApiTest):
maxDiff = None
with app.app_context():
config = migrate.get_config()
command.upgrade(config, "head")
seed()
def tearDown(self):
db.engine.dispose()
def test_create_profile(self):
user = User(auth_id="peter")
db.session.add(user)
db.session.commit()
login_data = {"username": "peter", "password": "geheim"}
login_response = self.client.post("/users/login",
data=json.dumps(login_data),
content_type="application/json")
def test_update_profile_unauthorised(self):
login_data = {"username": "klaus", "password": "jutta"}
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")
self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json)
response = self.client.post("/users/1/profile",
data=json.dumps({}),
content_type="application/json",
headers={"Authorization": "Bearer " + login_response.json["token"]})
self.assertEqual(response.status_code, 403)
def test_update_profile(self):
token = self.login("peter", "geheim")["token"]
data = {
"pronouns": "Herr Dr. Dr.",
"pronouns": "Monsieur",
"volunteerwork": "ja",
"freetext": "Hallo",
"address": {
"name": "Peeeda",
"street": "Bachstraße",
"house_number": "42x",
"additional": "oben",
"postcode": "23232",
"city": "Travemünde",
"country": "Deutschland"
},
"contacts": [{
"id": 1,
"contacttype": {
"id": 1,
"name": "Matrix"
},
"content": "@peeda:wtf-eg.de"
}, {
"contacttype": {
"name": "Rohrpost"
},
"content": "Ausgang 2"
}],
"skills": [{
"id": 1,
"skill": {
"id": 3,
"name": "Python"
},
"level": 4
}, {
"skill": {
"name": "Tschunkproduktion"
},
"level": 5
}],
"languages": [{
"id": 1,
"language": {
"id": "de",
"name": "Deutsch"
},
"level": 4
}, {
"language": {
"id": "es",
"name": "Spanisch"
},
"level": 2
}]
}
response = self.client.post("/users/1/profile",
data=json.dumps(data),
content_type="application/json",
headers={
"Authorization":
"Bearer " +
login_response.json["token"]
})
headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
with app.app_context():
user = User.query.filter(User.id == 1).first()
profile = user.profile
self.assertEqual("Herr Dr. Dr.", profile.pronouns)
self.assertEqual("Monsieur", profile.pronouns)
self.assertEqual("ja", profile.volunteerwork)
self.assertEqual("Hallo", profile.freetext)
address = profile.address
self.assertEqual(address.name, "Peeeda")
self.assertEqual(address.street, "Bachstraße")
self.assertEqual(address.house_number, "42x")
self.assertEqual(address.additional, "oben")
self.assertEqual(address.postcode, "23232")
self.assertEqual(address.city, "Travemünde")
self.assertEqual(address.country, "Deutschland")
contacts = profile.contacts
self.assertEqual(len(contacts), 2)
first_contact = contacts[0]
self.assertEqual(first_contact.contacttype.name, "Matrix")
self.assertEqual(first_contact.content, "@peeda:wtf-eg.de")
second_contact = contacts[1]
self.assertEqual(second_contact.contacttype.name, "Rohrpost")
self.assertEqual(second_contact.content, "Ausgang 2")
skills = profile.skills
self.assertEqual(len(skills), 2)
first_skill = skills[0]
self.assertEqual(first_skill.skill.id, 3)
self.assertEqual(first_skill.skill.name, "Python")
self.assertEqual(first_skill.level, 4)
second_skill = skills[1]
self.assertEqual(second_skill.skill.id, 13)
self.assertEqual(second_skill.skill.name, "Tschunkproduktion")
self.assertEqual(second_skill.level, 5)
languages = profile.languages
self.assertEqual(len(languages), 2)
first_language = languages[0]
self.assertEqual(first_language.language_id, "de")
self.assertEqual(first_language.level, 4)
second_language = languages[1]
self.assertEqual(second_language.language_id, "es")
self.assertEqual(second_language.level, 2)
def test_get_profile(self):
user = User(auth_id="peter")
db.session.add(user)
profile = Profile(user=user)
profile.nickname = "Popeter"
db.session.add(profile)
db.session.commit()
login_data = {"username": "peter", "password": "geheim"}
login_response = self.client.post("/users/login",
data=json.dumps(login_data),
content_type="application/json")
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")
self.assertEqual(login_response.status_code, 200)
self.assertIn("token", login_response.json)
response = self.client.get("/users/1/profile",
headers={
"Authorization":
"Bearer " + login_response.json["token"]
})
headers={"Authorization": "Bearer " + login_response.json["token"]})
self.assertEqual(response.status_code, 200)
self.assertEqual(
self.assertDictEqual(
response.json, {
"profile": {
"freetext": "",
"nickname": "Popeter",
"pronouns": "",
"volunteerwork": ""
"user_id": 1,
"nickname": "peternichtlustig",
"pronouns": "Herr Dr. Dr.",
"freetext": "Ich mag Kaffee",
"volunteerwork": "Gartenverein",
"address": {
"additional": "Hinterhaus",
"city": "Bielefeld",
"country": "Deutschland",
"house_number": "23i",
"id": 1,
"name": "Peter Nichtlustig",
"postcode": "13337",
"profile_id": 1,
"street": "Waldweg"
},
"contacts": [{
"id": 1,
"profile_id": 1,
"contacttype": {
"id": 1,
"name": "Matrix"
},
"content": "@peter:wtf-eg.de"
}, {
"id": 2,
"profile_id": 1,
"contacttype": {
"id": 2,
"name": "E-Mail"
},
"content": "peter@wtf-eg.de"
}],
"skills": [{
"profile_id": 1,
"skill": {
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
},
"level": 5
}, {
"profile_id": 1,
"skill": {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
},
"level": 3
}],
"languages": [{
"profile_id": 1,
"language": {
"id": "de",
"name": "Deutsch",
"icon_url": "/languages/de/icon"
},
"level": 5
}, {
"profile_id": 1,
"language": {
"id": "fr",
"name": "Französisch",
"icon_url": "/languages/fr/icon"
},
"level": 3
}]
}
})

View File

@ -1,34 +1,38 @@
from alembic import command
import unittest
from app import app, migrate
from ki.commands import seed
from ki.test.ApiTest import ApiTest
class TestSkillsEndpoint(unittest.TestCase):
def setUp(self):
app.debug = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
self.client = app.test_client()
with app.app_context():
config = migrate.get_config()
command.upgrade(config, "head")
seed()
class TestSkillsEndpoint(ApiTest):
def test_skills_options(self):
response = self.client.options("/skills")
self.assertEqual(response.status_code, 200)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
def test_get_skills1(self):
response = self.client.get("/skills?search=p")
token = self.login("peter", "geheim")["token"]
response = self.client.get("/skills?search=p", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200)
self.assertEqual(
{
"skills": [
{"id": 1, "name": "PHP"},
{"id": 3, "name": "Python"}
]
},
response.json
)
"skills": [{
"id": 1,
"name": "PHP",
"icon_url": "/skills/1/icon"
}, {
"id": 10,
"name": "PostgreSQL",
"icon_url": "/skills/10/icon"
}, {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}]
}, response.json)
self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
if __name__ == "main":