Compare commits
24 Commits
980415bd20
...
ext_skills
Author | SHA1 | Date | |
---|---|---|---|
6389258a60
|
|||
b8c2644086
|
|||
ec855a542d
|
|||
9a1f3f842a
|
|||
2d49c70f0f
|
|||
39f014db86
|
|||
922b20c990 | |||
5a84ba0689
|
|||
54f1c0f242
|
|||
178381b00c
|
|||
730878847b | |||
ea57f326b7 | |||
76e95311f0
|
|||
cd3f0f173b | |||
c37824785d
|
|||
a1502fea9c
|
|||
5bd6c827d8
|
|||
bbf8719fc9
|
|||
cd2a7853dd | |||
1b58c65666
|
|||
2f83e206e1
|
|||
8e087198a4 | |||
85b8e638a7
|
|||
020edffec7
|
@ -26,9 +26,6 @@ steps:
|
||||
from_secret: "docker_username"
|
||||
password:
|
||||
from_secret: "docker_password"
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
image_pull_secrets:
|
||||
- dockerconfig
|
||||
|
2
Pipfile
2
Pipfile
@ -16,6 +16,8 @@ sqlalchemy = "~=1.4.18"
|
||||
waitress = "~=2.0.0"
|
||||
pyyaml = "~=5.4.1"
|
||||
flask-cors = "~=3.0.10"
|
||||
ldap3 = "~=2.9"
|
||||
pymysql = "~=1.0.2"
|
||||
|
||||
[dev-packages]
|
||||
flake8 = "~=3.9.2"
|
||||
|
162
Pipfile.lock
generated
162
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "128a3b90d2e3d5876c5942fb256dba2d5e3d0b3a545cae6d51aeda6925a0f593"
|
||||
"sha256": "332b04c923ecb74bc066ee5348a664274ce87736981cc8e56e60070ff26e7d0e"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -135,6 +135,17 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6",
|
||||
"sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687",
|
||||
"sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70",
|
||||
"sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5",
|
||||
"sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.9.1"
|
||||
},
|
||||
"mako": {
|
||||
"hashes": [
|
||||
"sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab",
|
||||
@ -183,13 +194,39 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"pymysql": {
|
||||
"hashes": [
|
||||
"sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641",
|
||||
"sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.1"
|
||||
"version": "==2.8.2"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
@ -254,39 +291,39 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:0f6d467b67a7e5048f1408e8ea60d6caa70be5b386d0eebbf1185ab49cb8c7e4",
|
||||
"sha256:238d78b3110b7f7cffdb70bf9cda686e0d876a849bc78ba4d471aa7b1461f306",
|
||||
"sha256:25c0e0f3a7e8c19350086b3c0fe93c4def045cec053d749ef15da710c4d54c81",
|
||||
"sha256:2f60a2e599cf5cf5e5327ce60f2918b897e42ad9f405d10dd01e37869c0ce6fc",
|
||||
"sha256:38ee3a266afef2978e82824650457f70c5d74ec0cadec1b10fe5ed6f038eb5d0",
|
||||
"sha256:46361690f1e1c5385994a4caeb6e8126063ff593a5c635700bbc1245de793c1e",
|
||||
"sha256:46b99eab618cdc1c871ea707b7c52edc23cfea6c750740cd242ba62b5c84de7f",
|
||||
"sha256:4a67371752fd86d1d03a3b82d4e75404608f6f4d579b9676124079a22a40c79f",
|
||||
"sha256:525dd3c2205b11a2bc6d770bf1ec63bde0253fd754b4c19c399d27ddc9dad0d3",
|
||||
"sha256:6c8406c3d8c1c7d15da454de15d77f7bb48d14ede5db994f74226c348cf1050e",
|
||||
"sha256:6da83225a23eaf7b3f48f3d5f53c91b2cf00fbfa48b24a7a758160112dd3e123",
|
||||
"sha256:7150e5b543b466f45f668b352f7abda27998cc8035f051d1b7e9524ca9eb2f5f",
|
||||
"sha256:76fbc24311a3d039d6cd147d396719f606d96d1413f3816c028a48e29367f646",
|
||||
"sha256:854a7b15750e617e16f8d65dbc004f065a7963544b253b923f16109557648777",
|
||||
"sha256:86c079732328f1add097b0b8079cd532b5d28e207fac93e9d6ea5f487506deef",
|
||||
"sha256:8d860c62e3f51623ccd528d8fac44580501df557d4b467cc5581587fcf057719",
|
||||
"sha256:9675d5bc7e4f96a7bb2b54d14e9b269a5fb6e5d36ecc7d01f0f65bb9af3185f9",
|
||||
"sha256:9841762d114018c49483c089fa2d47f7e612e57666323f615913d7d7f46e9606",
|
||||
"sha256:9eb25bcf9161e2fcbe9eebe8e829719b2334e849183f0e496bf4b83722bcccfa",
|
||||
"sha256:aad3234a41340e9cf6184e621694e2a7233ba3f8aef9b1e6de8cba431b45ebd2",
|
||||
"sha256:b502b5e2f08500cc4b8d29bfc4f51d805adcbc00f8d149e98fda8aae85ddb644",
|
||||
"sha256:b86d83fefc8a8c394f3490c37e1953bc16c311a3d1d1cf91518793bfb9847fb4",
|
||||
"sha256:c0eb2cd3ad4967fcbdd9e066e8cd91fe2c23c671dbae9952f0b4d3d42832cc5f",
|
||||
"sha256:e0d48456e1aa4f0537f9c9af7be71e1f0659ff68bc1cd538ebc785f6b007bd0d",
|
||||
"sha256:eaee5dd378f6f0d7c3ec49aeeb26564d55ac0ad73b9b4688bf29e66deabddf73",
|
||||
"sha256:f14acb0fd16d404fda9370f93aace682f284340c89c3442ac747c5466ac7e2b5",
|
||||
"sha256:f6fc526bd70898489d02bf52c8f0632ab377592ae954d0c0a5bb38d618dddaa9",
|
||||
"sha256:fcd84e4d46a86291495d131a7824ba38d2e8278bda9425c50661a04633174319",
|
||||
"sha256:ff38ecf89c69a531a7326c2dae71982edfe2f805f3c016cdc5bfd1a04ebf80cb",
|
||||
"sha256:ff8bebc7a9d297dff2003460e01db2c20c63818b45fb19170f388b1a72fe5a14"
|
||||
"sha256:09dbb4bc01a734ccddbf188deb2a69aede4b3c153a72b6d5c6900be7fb2945b1",
|
||||
"sha256:12bac5fa1a6ea870bdccb96fe01610641dd44ebe001ed91ef7fcd980e9702db5",
|
||||
"sha256:1fdae7d980a2fa617d119d0dc13ecb5c23cc63a8b04ffcb5298f2c59d86851e9",
|
||||
"sha256:26daa429f039e29b1e523bf763bfab17490556b974c77b5ca7acb545b9230e9a",
|
||||
"sha256:36a089dc604032d41343d86290ce85d4e6886012eea73faa88001260abf5ff81",
|
||||
"sha256:39b5d36ab71f73c068cdcf70c38075511de73616e6c7fdd112d6268c2704d9f5",
|
||||
"sha256:4014978de28163cd8027434916a92d0f5bb1a3a38dff5e8bf8bff4d9372a9117",
|
||||
"sha256:44d23ea797a5e0be71bc5454b9ae99158ea0edc79e2393c6e9a2354de88329c0",
|
||||
"sha256:488608953385d6c127d2dcbc4b11f8d7f2f30b89f6bd27c01b042253d985cc2f",
|
||||
"sha256:5102b9face693e8b2db3b2539c7e1a5d9a5b4dc0d79967670626ffd2f710d6e6",
|
||||
"sha256:5908ea6c652a050d768580d01219c98c071e71910ab8e7b42c02af4010608397",
|
||||
"sha256:5d856cc50fd26fc8dd04892ed5a5a3d7eeb914fea2c2e484183e2d84c14926e0",
|
||||
"sha256:68393d3fd31469845b6ba11f5b4209edbea0b58506be0e077aafbf9aa2e21e11",
|
||||
"sha256:6a16c7c4452293da5143afa3056680db2d187b380b3ef4d470d4e29885720de3",
|
||||
"sha256:756f5d2f5b92d27450167247fb574b09c4cd192a3f8c2e493b3e518a204ee543",
|
||||
"sha256:891927a49b2363a4199763a9d436d97b0b42c65922a4ea09025600b81a00d17e",
|
||||
"sha256:9bfe882d5a1bbde0245dca0bd48da0976bd6634cf2041d2fdf0417c5463e40e5",
|
||||
"sha256:9fcbb4b4756b250ed19adc5e28c005b8ed56fdb5c21efa24c6822c0575b4964d",
|
||||
"sha256:a00d9c6d3a8afe1d1681cd8a5266d2f0ed684b0b44bada2ca82403b9e8b25d39",
|
||||
"sha256:a5e14cb0c0a4ac095395f24575a0e7ab5d1be27f5f9347f1762f21505e3ba9f1",
|
||||
"sha256:b48148ceedfb55f764562e04c00539bb9ea72bf07820ca15a594a9a049ff6b0e",
|
||||
"sha256:b7fb937c720847879c7402fe300cfdb2aeff22349fa4ea3651bca4e2d6555939",
|
||||
"sha256:bc34a007e604091ca3a4a057525efc4cefd2b7fe970f44d20b9cfa109ab1bddb",
|
||||
"sha256:c9373ef67a127799027091fa53449125351a8c943ddaa97bec4e99271dbb21f4",
|
||||
"sha256:d09a760b0a045b4d799102ae7965b5491ccf102123f14b2a8cc6c01d1021a2d9",
|
||||
"sha256:ec1be26cdccd60d180359a527d5980d959a26269a2c7b1b327a1eea0cab37ed8",
|
||||
"sha256:eedd76f135461cf237534a6dc0d1e0f6bb88a1dc193678fab48a11d223462da5",
|
||||
"sha256:f028ef6a1d828bc754852a022b2160e036202ac8658a6c7d34875aafd14a9a15",
|
||||
"sha256:f814d80844969b0d22ea63663da4de5ca1c434cfbae226188901e5d368792c17",
|
||||
"sha256:fd2102a8f8a659522719ed73865dff3d3cc76eb0833039dc473e0ad3041d04be"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.20"
|
||||
"version": "==1.4.22"
|
||||
},
|
||||
"waitress": {
|
||||
"hashes": [
|
||||
@ -306,12 +343,13 @@
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"backports.entry-points-selectable": {
|
||||
"hashes": [
|
||||
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||
"sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a",
|
||||
"sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"
|
||||
],
|
||||
"version": "==1.4.4"
|
||||
"markers": "python_version >= '2.7'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"binaryornot": {
|
||||
"hashes": [
|
||||
@ -350,6 +388,14 @@
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
||||
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.4"
|
||||
},
|
||||
"distlib": {
|
||||
"hashes": [
|
||||
"sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736",
|
||||
@ -374,19 +420,19 @@
|
||||
},
|
||||
"identify": {
|
||||
"hashes": [
|
||||
"sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421",
|
||||
"sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"
|
||||
"sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839",
|
||||
"sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==2.2.10"
|
||||
"version": "==2.2.11"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.2"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
@ -457,6 +503,14 @@
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
|
||||
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pre-commit": {
|
||||
"hashes": [
|
||||
"sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378",
|
||||
@ -526,11 +580,11 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.26.0"
|
||||
},
|
||||
"reuse": {
|
||||
"hashes": [
|
||||
@ -566,11 +620,11 @@
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467",
|
||||
"sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"
|
||||
"sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094",
|
||||
"sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.4.7"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==20.7.0"
|
||||
},
|
||||
"yapf": {
|
||||
"hashes": [
|
||||
|
38
README.md
38
README.md
@ -68,9 +68,18 @@ flake8
|
||||
|
||||
### Testbenutzer
|
||||
|
||||
#### Lokal ohne LDAP
|
||||
|
||||
Für ein Login ohne LDAP werden die Benutzer aus der [`auth.yml`](./data/auth.yml) benutzt.
|
||||
|
||||
|
||||
#### Lokal mit LDAP
|
||||
|
||||
Einen LDAP Server aufsetzen. Z.B. https://directory.apache.org/apacheds/
|
||||
|
||||
In der `.env` die LDAP Dinge ausfüllen (siehe [`env.dev`](./env.dev)).
|
||||
|
||||
|
||||
### Beispiel-Requests
|
||||
|
||||
Beispiele brauchen curl und jq.
|
||||
@ -113,6 +122,15 @@ curl -s \
|
||||
http://localhost:5000/users/1/profile
|
||||
```
|
||||
|
||||
Profilsuche nach Nickname:
|
||||
|
||||
```
|
||||
curl -s \
|
||||
-D "/dev/stderr" \
|
||||
-H "Authorization: Bearer 22e6c5fc-8a5a-440e-b1f4-018deb9fd24e" \
|
||||
http://localhost:5000/users/profiles
|
||||
```
|
||||
|
||||
|
||||
## Docker
|
||||
|
||||
@ -159,6 +177,26 @@ Für die Produktionsumgebung wird [waitress](https://docs.pylonsproject.org/proj
|
||||
[`run_prod.py`](./run_prod.py) führt die DB Migrationen aus und startet den Server.
|
||||
|
||||
|
||||
## Integrationsumgebung
|
||||
|
||||
Per [`docker-compose`](https://docs.docker.com/compose/) kann eine Integrationsumgebung gestartet werden.
|
||||
Beide Projekte müssen nebeneinander ausgecheckt sein:
|
||||
|
||||
```
|
||||
./
|
||||
ki-backend
|
||||
ki-frontend
|
||||
```
|
||||
|
||||
Alles starten:
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Dann http://localhost:13337 aufrufen.
|
||||
|
||||
|
||||
## Lizenzen
|
||||
|
||||
Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation.
|
||||
|
19
app.py
19
app.py
@ -8,25 +8,36 @@ 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.logging import default_handler
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from ldap3.utils.log import logger as ldap3_logger
|
||||
from ldap3.utils.log import set_library_log_detail_level, BASIC
|
||||
|
||||
load_dotenv(find_dotenv())
|
||||
|
||||
app = Flask(__name__)
|
||||
loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING)
|
||||
loglevel = int(loglevel)
|
||||
app.logger.setLevel(loglevel)
|
||||
logging.basicConfig(level=loglevel)
|
||||
logging.debug("Hello from KI")
|
||||
|
||||
app = Flask(__name__)
|
||||
set_library_log_detail_level(BASIC)
|
||||
ldap3_logger.addHandler(default_handler)
|
||||
|
||||
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", "*")
|
||||
|
||||
app.config["KI_AUTH"] = os.getenv("KI_AUTH")
|
||||
app.config["KI_LDAP_URL"] = os.getenv("KI_LDAP_URL")
|
||||
app.config["KI_LDAP_ROOT_DN"] = os.getenv("KI_LDAP_ROOT_DN")
|
||||
|
||||
CORS(app)
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
logging.debug("Hello from KI")
|
||||
|
||||
from ki import module # noqa
|
||||
|
@ -7,3 +7,5 @@ peter:
|
||||
password: geheim
|
||||
klaus:
|
||||
password: jutta
|
||||
dieter:
|
||||
password: hunger
|
||||
|
@ -11,3 +11,116 @@ id,name
|
||||
10,PostgreSQL
|
||||
11,SQLite
|
||||
12,Node.js
|
||||
13,C++
|
||||
14,C
|
||||
15,VHDL
|
||||
16,go
|
||||
17,Perl
|
||||
18,3D-Druck
|
||||
19,ABAP
|
||||
20,Android
|
||||
21,Ansible
|
||||
22,Arduino
|
||||
23,Bash
|
||||
24,batou
|
||||
25,bind
|
||||
26,Buchführung
|
||||
27,C#
|
||||
28,CAD
|
||||
29,CAM
|
||||
30,Cobol
|
||||
31,CRM
|
||||
32,CSS
|
||||
33,D
|
||||
34,Debian
|
||||
35,Delphi
|
||||
36,DevOPS
|
||||
37,Discourse
|
||||
38,Django
|
||||
39,DNS
|
||||
40,Dovecot
|
||||
41,Elasticsearch
|
||||
42,Emacs
|
||||
43,Email
|
||||
44,ERP
|
||||
45,ESP
|
||||
46,Excel
|
||||
47,Fahrdienstleitung
|
||||
48,Fedora
|
||||
49,FLOSS
|
||||
50,Geographie
|
||||
51,Geologie
|
||||
52,Gnome
|
||||
53,GPS
|
||||
54,Grafana
|
||||
55,GrayLog
|
||||
56,GSM
|
||||
57,GTK
|
||||
58,HTML
|
||||
59,IBM Z
|
||||
60,IMAP
|
||||
61,Ionic
|
||||
62,iOS
|
||||
63,Java
|
||||
64,Kryptographie
|
||||
65,LDAP
|
||||
66,LibreOffice
|
||||
67,Linux
|
||||
68,Literaturgeschichte
|
||||
69,Lithographie
|
||||
70,Lucene
|
||||
71,Mailman
|
||||
72,MariaDB
|
||||
73,Markdown
|
||||
74,Marketing
|
||||
75,Microsoft Office
|
||||
76,Monitoring
|
||||
77,Nagios
|
||||
78,nähen
|
||||
79,NixOS
|
||||
80,odoo
|
||||
81,OpenOffice
|
||||
82,OpenPGP
|
||||
83,OpenStreetMap
|
||||
84,openSUSE
|
||||
85,Oracle
|
||||
86,Percona
|
||||
87,PGP
|
||||
88,Plone
|
||||
89,Postfix
|
||||
90,PowerDNS
|
||||
91,PowerPC
|
||||
92,Projektmanagement
|
||||
93,puppet
|
||||
94,Qt
|
||||
95,R/3
|
||||
96,RedHat
|
||||
97,RHEL
|
||||
98,S/4
|
||||
99,Salt
|
||||
100,SAP
|
||||
101,Scrum
|
||||
102,ScummVM
|
||||
103,Sensu
|
||||
104,sh
|
||||
105,Siemens S5
|
||||
106,Siemens S7
|
||||
107,Simatic
|
||||
108,SLES
|
||||
109,Spring
|
||||
110,SQL
|
||||
111,Stenographie
|
||||
112,Steuerrecht
|
||||
113,Teppich knüpfen
|
||||
114,TeX
|
||||
115,Verlagswesen
|
||||
116,vi(m)
|
||||
117,Windows 10
|
||||
118,Windows 2000
|
||||
119,Windows 2012R2
|
||||
120,Windows 7
|
||||
121,Windows NT
|
||||
122,Windows XP
|
||||
123,x86
|
||||
124,Zope
|
||||
125,zsh
|
||||
|
|
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
# SPDX-FileCopyrightText: 2021 WTF Kooperative eG <https://wtf-eg.de/>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
ki_backend:
|
||||
build: .
|
||||
restart: "no"
|
||||
ports:
|
||||
- "13338:5000"
|
||||
volumes:
|
||||
- "./env.dev:/app/.env"
|
||||
|
||||
ki_frontend:
|
||||
build: ./../ki-frontend
|
||||
restart: "no"
|
||||
ports:
|
||||
- "13337:80"
|
||||
volumes:
|
||||
- "../ki-frontend/public/config.js.int:/user/share/nginx/html/config.js"
|
5
env.dev
5
env.dev
@ -9,7 +9,12 @@ CORS_ORIGINS=*
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
|
||||
# auth method: file or ldap
|
||||
KI_AUTH=file
|
||||
|
||||
# ldap auth only
|
||||
KI_LDAP_URL=ldap://localhost:10389
|
||||
KI_LDAP_ROOT_DN=dc=example,dc=com
|
||||
|
||||
# 10 = debug
|
||||
KI_LOGLEVEL=10
|
||||
|
@ -6,7 +6,8 @@ import csv
|
||||
import logging
|
||||
|
||||
from app import app, db
|
||||
from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSkill, User
|
||||
from ki.models import Address, Contact, ContactType, Language, Skill, Profile, ProfileLanguage, ProfileSearchtopic, \
|
||||
ProfileSkill, User
|
||||
|
||||
|
||||
def seed_contacttypes():
|
||||
@ -25,12 +26,37 @@ def seed_contacttypes():
|
||||
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"]))
|
||||
|
||||
|
||||
def seed_user(nickname, visible=False, skills=[], languages=[], volunteerwork="", availability="", freetext=""):
|
||||
app.logger.info(f"seeding {nickname} \\o/")
|
||||
|
||||
user = User(auth_id=nickname)
|
||||
db.session.add(user)
|
||||
|
||||
profile = Profile(nickname=nickname,
|
||||
pronouns="",
|
||||
volunteerwork=volunteerwork,
|
||||
availability=availability,
|
||||
freetext=freetext,
|
||||
visible=visible,
|
||||
user=user)
|
||||
|
||||
for skill_data in skills:
|
||||
skill = ProfileSkill(profile=profile, skill_id=skill_data[0], level=skill_data[1])
|
||||
db.session.add(skill)
|
||||
|
||||
for language_data in languages:
|
||||
language = ProfileLanguage(profile=profile, language_id=language_data[0], level=language_data[1])
|
||||
db.session.add(language)
|
||||
|
||||
db.session.add(profile)
|
||||
|
||||
|
||||
def seed(dev: bool):
|
||||
seed_contacttypes()
|
||||
|
||||
skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv"
|
||||
|
||||
logging.info("importing skills")
|
||||
app.logger.info("importing skills")
|
||||
|
||||
with open(skill_seed_file_path) as skills_file:
|
||||
skills_csv_reader = csv.DictReader(skills_file)
|
||||
@ -42,7 +68,7 @@ def seed(dev: bool):
|
||||
if db_skill is None:
|
||||
db.session.add(Skill(id=int(skill["id"]), name=skill["name"]))
|
||||
|
||||
logging.info("importing languages")
|
||||
app.logger.info("importing languages")
|
||||
|
||||
iso_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/iso_639_1.csv"
|
||||
|
||||
@ -57,7 +83,7 @@ def seed(dev: bool):
|
||||
db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
|
||||
|
||||
if dev:
|
||||
logging.info("seeding peter :)")
|
||||
app.logger.info("seeding peter :)")
|
||||
|
||||
peter = User(auth_id="peter")
|
||||
db.session.add(peter)
|
||||
@ -92,15 +118,33 @@ def seed(dev: bool):
|
||||
peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5)
|
||||
db.session.add(peters_php_skill)
|
||||
|
||||
peters_python_searchtopic = ProfileSearchtopic(profile=peters_profile, skill_id=3)
|
||||
db.session.add(peters_python_searchtopic)
|
||||
|
||||
peters_php_searchtopic = ProfileSearchtopic(profile=peters_profile, skill_id=1)
|
||||
db.session.add(peters_php_searchtopic)
|
||||
|
||||
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")
|
||||
seed_user("klaus")
|
||||
|
||||
klaus = User(auth_id="klaus")
|
||||
db.session.add(klaus)
|
||||
seed_user("dirtydieter",
|
||||
visible=True,
|
||||
volunteerwork="Müll sammeln",
|
||||
availability="Nur nachts",
|
||||
freetext="1001010010111!!!",
|
||||
skills=[(Skill.skill_id_php, 5)])
|
||||
|
||||
seed_user("jutta",
|
||||
visible=True,
|
||||
languages=[("fr", 5)],
|
||||
skills=[(Skill.skill_id_php, 3), (Skill.skill_id_mysql, 4)])
|
||||
seed_user("giesela", visible=True, skills=[(Skill.skill_id_mysql, 3), (Skill.skill_id_postgresql, 5)])
|
||||
seed_user("bertha", visible=False, skills=[(Skill.skill_id_sqlite, 3), (Skill.skill_id_postgresql, 5)])
|
||||
seed_user("monique", visible=True, languages=[("fr", 4)])
|
||||
|
||||
db.session.commit()
|
||||
|
53
ki/auth.py
53
ki/auth.py
@ -5,11 +5,28 @@
|
||||
import uuid
|
||||
import yaml
|
||||
|
||||
from ldap3 import Server, Connection, ALL
|
||||
|
||||
from app import app, db
|
||||
from ki.models import User, Token
|
||||
|
||||
|
||||
def auth(username, password):
|
||||
def create_user_token(username):
|
||||
user = User.query.filter(User.auth_id.__eq__(username)).first()
|
||||
|
||||
if user is None:
|
||||
user = User(auth_id=username)
|
||||
db.session.add(user)
|
||||
|
||||
token = Token(token=str(uuid.uuid4()), user=user)
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
return token
|
||||
|
||||
|
||||
def file_auth(username, password):
|
||||
app.logger.debug("performing file authentication")
|
||||
|
||||
auth_file_path = app.config["KI_DATA_DIR"] + "/auth.yml"
|
||||
|
||||
with open(auth_file_path, "r") as auth_file_stream:
|
||||
@ -23,14 +40,32 @@ def auth(username, password):
|
||||
if auth_user["password"] != password:
|
||||
return None
|
||||
|
||||
user = User.query.filter(User.auth_id.__eq__(username)).first()
|
||||
return create_user_token(username)
|
||||
|
||||
if user is None:
|
||||
user = User(auth_id=username)
|
||||
db.session.add(user)
|
||||
|
||||
token = Token(token=str(uuid.uuid4()), user=user)
|
||||
db.session.add(token)
|
||||
db.session.commit()
|
||||
def ldap_auth(username, password):
|
||||
app.logger.debug("performing LDAP authentication")
|
||||
|
||||
return token
|
||||
server = Server(app.config['KI_LDAP_URL'], get_info=ALL)
|
||||
root_dn = app.config['KI_LDAP_ROOT_DN']
|
||||
ldap_user = f"cn={username},{root_dn}"
|
||||
|
||||
app.logger.debug(f"server: {server}")
|
||||
connection = Connection(server, user=ldap_user, password=password)
|
||||
|
||||
if connection.bind():
|
||||
connection.unbind()
|
||||
return create_user_token(username)
|
||||
|
||||
connection.unbind()
|
||||
return None
|
||||
|
||||
|
||||
def auth(username, password):
|
||||
if app.config['KI_AUTH'] == 'file':
|
||||
return file_auth(username, password)
|
||||
|
||||
if app.config['KI_AUTH'] == 'ldap':
|
||||
return ldap_auth(username, password)
|
||||
|
||||
raise RuntimeError('unknown auth method')
|
||||
|
@ -2,4 +2,5 @@
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from ki.handlers.find_profiles import find_profiles # noqa
|
||||
from ki.handlers.update_profile import update_profile # noqa
|
||||
|
44
ki/handlers/find_profiles.py
Normal file
44
ki/handlers/find_profiles.py
Normal file
@ -0,0 +1,44 @@
|
||||
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from flask import make_response, request
|
||||
|
||||
from ki.models import Profile, ProfileSkill, Skill, ProfileLanguage, Language
|
||||
|
||||
|
||||
def find_profiles():
|
||||
page = int(request.args.get("page", 1))
|
||||
|
||||
if page < 1:
|
||||
return make_response({"messages": {"page": "Die angefragte Seite muss mindestens 1 sein"}}, 400)
|
||||
|
||||
page_size = int(request.args.get("page_size", 20))
|
||||
|
||||
if page_size > 100:
|
||||
return make_response({"messages": {"page_size": "Die maximale Anzahl Einträge pro Seite beträgt 100"}}, 400)
|
||||
|
||||
query = Profile.query.filter(Profile.visible.is_(True)) \
|
||||
.join(Profile.skills, isouter=True).join(ProfileSkill.skill, isouter=True) \
|
||||
.join(Profile.languages, isouter=True).join(ProfileLanguage.language, isouter=True)
|
||||
|
||||
if "search" in request.args:
|
||||
terms = request.args["search"].split(" ")
|
||||
for term in terms:
|
||||
query = query.filter(
|
||||
Profile.nickname.like(f"%{term}%") | Skill.name.like(f"%{term}%") | Language.name.like(f"%{term}%"))
|
||||
|
||||
if "nickname" in request.args:
|
||||
nickname = request.args.get("nickname")
|
||||
query = query.filter(Profile.nickname.like(f"%{nickname}%"))
|
||||
|
||||
count = query.distinct(Profile.id).count()
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
db_profiles = query.limit(page_size).offset(offset).all()
|
||||
api_profiles = []
|
||||
|
||||
for db_profile in db_profiles:
|
||||
api_profiles.append(db_profile.to_dict())
|
||||
|
||||
return make_response({"total": count, "profiles": api_profiles})
|
@ -5,7 +5,8 @@
|
||||
from flask import make_response, request
|
||||
from sqlalchemy import not_
|
||||
|
||||
from ki.models import Address, Contact, ContactType, Language, User, Profile, ProfileLanguage, ProfileSkill, Skill
|
||||
from ki.models import Address, Contact, ContactType, Language, User, Profile, ProfileLanguage, ProfileSearchtopic, \
|
||||
ProfileSkill, Skill
|
||||
from app import db
|
||||
|
||||
|
||||
@ -71,6 +72,30 @@ def update_skills(profile, skills_data):
|
||||
not_(ProfileSkill.skill_id.in_(profile_skill_ids))).delete()
|
||||
|
||||
|
||||
def update_searchtopics(profile, searchtopics_data):
|
||||
profile_searchtopics_ids = []
|
||||
|
||||
for searchtopic_data in searchtopics_data:
|
||||
skill_name = searchtopic_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_searchtopic = ProfileSearchtopic.query.filter(ProfileSearchtopic.profile == profile,
|
||||
ProfileSearchtopic.skill == skill).first()
|
||||
|
||||
if (profile_searchtopic is None):
|
||||
profile_searchtopic = ProfileSearchtopic(profile=profile, skill=skill)
|
||||
db.session.add(profile_searchtopic)
|
||||
|
||||
profile_searchtopics_ids.append(skill.id)
|
||||
|
||||
ProfileSearchtopic.query.filter(ProfileSearchtopic.profile == profile,
|
||||
not_(ProfileSearchtopic.skill_id.in_(profile_searchtopics_ids))).delete()
|
||||
|
||||
|
||||
def update_contacts(profile, contacts_data):
|
||||
contact_ids_to_be_deleted = list(map(lambda c: c.id, profile.contacts))
|
||||
|
||||
@ -117,6 +142,7 @@ def update_profile(user_id: int):
|
||||
update_address(profile, request.json.get("address", {}))
|
||||
update_contacts(profile, request.json.get("contacts", {}))
|
||||
update_skills(profile, request.json.get("skills", {}))
|
||||
update_searchtopics(profile, request.json.get("searchtopics"))
|
||||
update_languages(profile, request.json.get("languages", {}))
|
||||
|
||||
db.session.commit()
|
||||
|
57
ki/models.py
57
ki/models.py
@ -41,6 +41,7 @@ class Profile(db.Model):
|
||||
contacts = relationship("Contact")
|
||||
address = relationship("Address", uselist=False, back_populates="profile")
|
||||
skills = relationship("ProfileSkill", back_populates="profile")
|
||||
searchtopics = relationship("ProfileSearchtopic", back_populates="profile")
|
||||
languages = relationship("ProfileLanguage", back_populates="profile")
|
||||
|
||||
def to_dict(self):
|
||||
@ -52,9 +53,10 @@ class Profile(db.Model):
|
||||
"availability": self.availability,
|
||||
"freetext": self.freetext,
|
||||
"visible": self.visible,
|
||||
"address": self.address.to_dict(),
|
||||
"address": self.address.to_dict() if self.address else None,
|
||||
"contacts": list(map(lambda contact: contact.to_dict(), self.contacts)),
|
||||
"skills": list(map(lambda skill: skill.to_dict(), self.skills)),
|
||||
"searchtopics": list(map(lambda searchtopic: searchtopic.to_dict(), self.searchtopics)),
|
||||
"languages": list(map(lambda language: language.to_dict(), self.languages))
|
||||
}
|
||||
|
||||
@ -131,18 +133,39 @@ class Address(db.Model):
|
||||
|
||||
|
||||
class Skill(db.Model):
|
||||
skill_id_php = 1
|
||||
skill_id_python = 3
|
||||
skill_id_sqlalchemy = 7
|
||||
skill_id_mysql = 9
|
||||
skill_id_postgresql = 10
|
||||
skill_id_sqlite = 11
|
||||
|
||||
__tablename__ = "skill"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(25), unique=True, nullable=False)
|
||||
|
||||
profiles = relationship("ProfileSkill", back_populates="skill")
|
||||
searchtopics = relationship("ProfileSearchtopic", back_populates="skill")
|
||||
|
||||
def to_dict(self):
|
||||
return {"id": self.id, "name": self.name, "icon_url": "/skills/{}/icon".format(self.id)}
|
||||
|
||||
|
||||
class ProfileSkill(db.Model):
|
||||
level1_text = "bis 6 Monate"
|
||||
level2_text = "bis 1 Jahr"
|
||||
level3_text = "bis 3 Jahre"
|
||||
level4_text = "bis 5 Jahre"
|
||||
level5_text = "mehr als 5 Jahre"
|
||||
level_texts = {
|
||||
1: level1_text,
|
||||
2: level2_text,
|
||||
3: level3_text,
|
||||
4: level4_text,
|
||||
5: level5_text,
|
||||
}
|
||||
|
||||
__tablename__ = "profile_skill"
|
||||
|
||||
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True)
|
||||
@ -156,11 +179,24 @@ class ProfileSkill(db.Model):
|
||||
return {"profile_id": self.profile_id, "skill": self.skill.to_dict(), "level": self.level}
|
||||
|
||||
|
||||
class ProfileSearchtopic(db.Model):
|
||||
__tablename__ = "profile_searchtopic"
|
||||
|
||||
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True)
|
||||
skill_id = Column(Integer, ForeignKey("skill.id"), primary_key=True)
|
||||
|
||||
profile = relationship("Profile", back_populates="searchtopics")
|
||||
skill = relationship("Skill", back_populates="searchtopics")
|
||||
|
||||
def to_dict(self):
|
||||
return {"profile_id": self.profile_id, "skill": self.skill.to_dict()}
|
||||
|
||||
|
||||
class Language(db.Model):
|
||||
__tablename__ = "language"
|
||||
|
||||
id = Column(String(2), primary_key=True)
|
||||
name = Column(String(25), nullable=False)
|
||||
id = Column(String(4), primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
|
||||
profiles = relationship("ProfileLanguage", back_populates="language")
|
||||
|
||||
@ -169,10 +205,23 @@ class Language(db.Model):
|
||||
|
||||
|
||||
class ProfileLanguage(db.Model):
|
||||
level1_text = "keine Angabe"
|
||||
level2_text = "Grundkenntnisse"
|
||||
level3_text = "Gut"
|
||||
level4_text = "Fließend"
|
||||
level5_text = "Muttersprache"
|
||||
level_texts = {
|
||||
1: level1_text,
|
||||
2: level2_text,
|
||||
3: level3_text,
|
||||
4: level4_text,
|
||||
5: level5_text,
|
||||
}
|
||||
|
||||
__tablename__ = "profile_language"
|
||||
|
||||
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True)
|
||||
language_id = Column(Integer, ForeignKey("language.id"), primary_key=True)
|
||||
language_id = Column(String(4), ForeignKey("language.id"), primary_key=True)
|
||||
level = Column(SmallInteger, nullable=False)
|
||||
|
||||
profile = relationship("Profile", back_populates="languages")
|
||||
|
18
ki/routes.py
18
ki/routes.py
@ -7,10 +7,14 @@ from flask import g, make_response, request, send_file
|
||||
from functools import wraps
|
||||
|
||||
from ki.auth import auth
|
||||
from ki.handlers import find_profiles as find_profiles_handler
|
||||
from ki.handlers import update_profile as update_profile_handler
|
||||
from ki.models import ContactType, Language, Skill, Token, User
|
||||
from app import app
|
||||
|
||||
content_type_svg = "image/svg+xml"
|
||||
content_type_png = "image/png"
|
||||
|
||||
|
||||
def token_auth(func):
|
||||
@wraps(func)
|
||||
@ -70,22 +74,22 @@ def handle_icon_request(model, id, path):
|
||||
icon_svg_path = icon_base_path + ".svg"
|
||||
|
||||
if os.path.exists(icon_svg_path):
|
||||
return send_file(icon_svg_path, mimetype="image/svg")
|
||||
return send_file(icon_svg_path, mimetype=content_type_svg)
|
||||
|
||||
icon_png_path = icon_base_path + ".png"
|
||||
|
||||
if os.path.exists(icon_png_path):
|
||||
return send_file(icon_png_path, mimetype="image/png")
|
||||
return send_file(icon_png_path, mimetype=content_type_png)
|
||||
|
||||
unknown_svg_path = path + "unknown.svg"
|
||||
|
||||
if os.path.exists(unknown_svg_path):
|
||||
return send_file(unknown_svg_path, mimetype="image/svg")
|
||||
return send_file(unknown_svg_path, mimetype=content_type_svg)
|
||||
|
||||
unknown_png_path = path + "unknown.png"
|
||||
|
||||
if os.path.exists(unknown_png_path):
|
||||
return send_file(unknown_png_path, mimetype="image/png")
|
||||
return send_file(unknown_png_path, mimetype=content_type_png)
|
||||
|
||||
return make_response({"error": "icon not found"}, 404)
|
||||
|
||||
@ -143,6 +147,12 @@ def get_contacttypes():
|
||||
return handle_completion_request(ContactType, "contacttypes")
|
||||
|
||||
|
||||
@app.route("/users/profiles")
|
||||
@token_auth
|
||||
def find_profiles():
|
||||
return find_profiles_handler()
|
||||
|
||||
|
||||
@app.route("/skills")
|
||||
@token_auth
|
||||
def get_skills():
|
||||
|
@ -8,6 +8,7 @@ import unittest
|
||||
|
||||
from app import app, db, migrate
|
||||
from ki.actions import seed
|
||||
from ki.models import Skill
|
||||
|
||||
|
||||
class ApiTest(unittest.TestCase):
|
||||
@ -15,8 +16,10 @@ class ApiTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
app.debug = True
|
||||
app.config["KI_AUTH"] = "file"
|
||||
app.config["TESTING"] = True
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
|
||||
self.client = app.test_client()
|
||||
|
||||
with app.app_context():
|
||||
@ -24,6 +27,8 @@ class ApiTest(unittest.TestCase):
|
||||
command.upgrade(config, "head")
|
||||
|
||||
seed(True)
|
||||
max_skill = Skill.query.order_by(Skill.id.desc()).first()
|
||||
self.max_skill_id = max_skill.id
|
||||
|
||||
def tearDown(self):
|
||||
db.drop_all()
|
||||
|
92
ki/test/test_find_profiles_endpoint.py
Normal file
92
ki/test/test_find_profiles_endpoint.py
Normal file
@ -0,0 +1,92 @@
|
||||
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import unittest
|
||||
|
||||
from ki.test.ApiTest import ApiTest
|
||||
|
||||
|
||||
class TestFindProfilesEndpoint(ApiTest):
|
||||
def test_find_profiles_options(self):
|
||||
response = self.client.options("/users/profiles")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Access-Control-Allow-Origin", response.headers)
|
||||
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_find_nobody(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?nickname=horsthorsthorst",
|
||||
headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {"total": 0, "profiles": []})
|
||||
|
||||
def test_find_sql_specialchars(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?nickname=%22%27%25", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json, {"total": 0, "profiles": []})
|
||||
|
||||
def test_find_all(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 4}, response.json)
|
||||
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
|
||||
|
||||
def test_find_dieter(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?search=dieter%20php", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 1}, response.json)
|
||||
self.assertDictContainsSubset({"nickname": "dirtydieter"}, response.json["profiles"][0])
|
||||
|
||||
def test_not_find_dieter(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?search=dieter%20sqlite",
|
||||
headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 0}, response.json)
|
||||
|
||||
def test_find_sql(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?search=sql", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 2}, response.json)
|
||||
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
|
||||
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][1])
|
||||
|
||||
def test_find_postgres(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?search=post", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 1}, response.json)
|
||||
self.assertDictContainsSubset({"nickname": "giesela"}, response.json["profiles"][0])
|
||||
|
||||
def test_find_php_franzosen(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?search=php%20franz", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 1}, response.json)
|
||||
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
|
||||
|
||||
def test_find_franzosen(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/profiles?search=französisch", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictContainsSubset({"total": 2}, response.json)
|
||||
self.assertDictContainsSubset({"nickname": "jutta"}, response.json["profiles"][0])
|
||||
self.assertDictContainsSubset({"nickname": "monique"}, response.json["profiles"][1])
|
||||
|
||||
|
||||
if __name__ == "main":
|
||||
unittest.main()
|
38
ki/test/test_languages_endpoint.py
Normal file
38
ki/test/test_languages_endpoint.py
Normal file
@ -0,0 +1,38 @@
|
||||
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import unittest
|
||||
|
||||
from ki.test.ApiTest import ApiTest
|
||||
|
||||
|
||||
class TestLanguagesEndpoint(ApiTest):
|
||||
def test_skills_options(self):
|
||||
response = self.client.options("/languages")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Access-Control-Allow-Origin", response.headers)
|
||||
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_search_languages_fr(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/languages?search=fr", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual({"languages": [{
|
||||
"id": "fr",
|
||||
"name": "Französisch",
|
||||
"icon_url": "/languages/fr/icon"
|
||||
}]}, response.json)
|
||||
self.assertIn("Access-Control-Allow-Origin", response.headers)
|
||||
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_get_fr_icon(self):
|
||||
response = self.client.get("/languages/fr/icon")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Content-Type", response.headers)
|
||||
self.assertEqual(response.headers["Content-Type"], "image/png")
|
||||
|
||||
|
||||
if __name__ == "main":
|
||||
unittest.main()
|
@ -71,6 +71,19 @@ class TestProfileEndpoint(ApiTest):
|
||||
},
|
||||
"level": 5
|
||||
}],
|
||||
"searchtopics": [{
|
||||
"profile_id": 1,
|
||||
"skill": {
|
||||
"id": 3,
|
||||
"name": "Python",
|
||||
"icon_url": "/skills/3/icon"
|
||||
}
|
||||
}, {
|
||||
"profile_id": 1,
|
||||
"skill": {
|
||||
"name": "Assembler"
|
||||
}
|
||||
}],
|
||||
"languages": [{
|
||||
"id": 1,
|
||||
"language": {
|
||||
@ -130,10 +143,21 @@ class TestProfileEndpoint(ApiTest):
|
||||
self.assertEqual(first_skill.level, 4)
|
||||
|
||||
second_skill = skills[1]
|
||||
self.assertEqual(second_skill.skill.id, 13)
|
||||
self.assertEqual(second_skill.skill.id, self.max_skill_id + 1)
|
||||
self.assertEqual(second_skill.skill.name, "Tschunkproduktion")
|
||||
self.assertEqual(second_skill.level, 5)
|
||||
|
||||
searchtopics = profile.searchtopics
|
||||
self.assertEqual(len(searchtopics), 2)
|
||||
|
||||
first_searchtopic = searchtopics[0]
|
||||
self.assertEqual(first_searchtopic.skill.id, 3)
|
||||
self.assertEqual(first_searchtopic.skill.name, "Python")
|
||||
|
||||
second_searchtopic = searchtopics[1]
|
||||
self.assertEqual(second_searchtopic.skill.id, self.max_skill_id + 2)
|
||||
self.assertEqual(second_searchtopic.skill.name, "Assembler")
|
||||
|
||||
languages = profile.languages
|
||||
self.assertEqual(len(languages), 2)
|
||||
|
||||
@ -150,7 +174,14 @@ class TestProfileEndpoint(ApiTest):
|
||||
response = self.client.get("/users/1/profile")
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_get_profile(self):
|
||||
def test_get_visible_proifle(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/users/3/profile", headers={"Authorization": f"Bearer {token}"})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_get_own_profile(self):
|
||||
login_data = {"username": "peter", "password": "geheim"}
|
||||
login_response = self.client.post("/users/login", data=json.dumps(login_data), content_type="application/json")
|
||||
|
||||
@ -216,6 +247,21 @@ class TestProfileEndpoint(ApiTest):
|
||||
},
|
||||
"level": 3
|
||||
}],
|
||||
"searchtopics": [{
|
||||
"profile_id": 1,
|
||||
"skill": {
|
||||
"id": 1,
|
||||
"name": "PHP",
|
||||
"icon_url": "/skills/1/icon"
|
||||
}
|
||||
}, {
|
||||
"profile_id": 1,
|
||||
"skill": {
|
||||
"id": 3,
|
||||
"name": "Python",
|
||||
"icon_url": "/skills/3/icon"
|
||||
}
|
||||
}],
|
||||
"languages": [{
|
||||
"profile_id": 1,
|
||||
"language": {
|
||||
|
@ -14,30 +14,39 @@ class TestSkillsEndpoint(ApiTest):
|
||||
self.assertIn("Access-Control-Allow-Origin", response.headers)
|
||||
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_get_skills1(self):
|
||||
def test_find_skills_php(self):
|
||||
token = self.login("peter", "geheim")["token"]
|
||||
|
||||
response = self.client.get("/skills?search=php", headers={"Authorization": "Bearer " + token})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertGreaterEqual(len(response.json['skills']), 1)
|
||||
self.assertIn({"id": 1, "name": "PHP", "icon_url": "/skills/1/icon"}, response.json['skills'])
|
||||
|
||||
self.assertIn("Access-Control-Allow-Origin", response.headers)
|
||||
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_find_skills_p(self):
|
||||
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",
|
||||
"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.assertGreaterEqual(len(response.json['skills']), 3)
|
||||
|
||||
self.assertIn("Access-Control-Allow-Origin", response.headers)
|
||||
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_get_php_skill_icon(self):
|
||||
response = self.client.get("/skills/1/icon")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Content-Type", response.headers)
|
||||
self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8")
|
||||
|
||||
def test_get_fallback_skill_icon(self):
|
||||
response = self.client.get("/skills/2/icon")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Content-Type", response.headers)
|
||||
self.assertEqual(response.headers["Content-Type"], "image/svg+xml; charset=utf-8")
|
||||
|
||||
|
||||
if __name__ == "main":
|
||||
unittest.main()
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Initial migration.
|
||||
|
||||
Revision ID: 808fe55111df
|
||||
Revision ID: 9183e2335b05
|
||||
Revises:
|
||||
Create Date: 2021-07-05 20:13:50.560579
|
||||
Create Date: 2021-08-02 21:51:30.400680
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '808fe55111df'
|
||||
revision = '9183e2335b05'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
@ -24,8 +24,8 @@ def upgrade():
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('language',
|
||||
sa.Column('id', sa.String(length=2), nullable=False),
|
||||
sa.Column('name', sa.String(length=25), nullable=False),
|
||||
sa.Column('id', sa.String(length=4), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('profile',
|
||||
@ -71,12 +71,19 @@ def upgrade():
|
||||
)
|
||||
op.create_table('profile_language',
|
||||
sa.Column('profile_id', sa.Integer(), nullable=False),
|
||||
sa.Column('language_id', sa.Integer(), nullable=False),
|
||||
sa.Column('language_id', sa.String(length=4), nullable=False),
|
||||
sa.Column('level', sa.SmallInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['language_id'], ['language.id'], ),
|
||||
sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ),
|
||||
sa.PrimaryKeyConstraint('profile_id', 'language_id')
|
||||
)
|
||||
op.create_table('profile_searchtopic',
|
||||
sa.Column('profile_id', sa.Integer(), nullable=False),
|
||||
sa.Column('skill_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ),
|
||||
sa.ForeignKeyConstraint(['skill_id'], ['skill.id'], ),
|
||||
sa.PrimaryKeyConstraint('profile_id', 'skill_id')
|
||||
)
|
||||
op.create_table('profile_skill',
|
||||
sa.Column('profile_id', sa.Integer(), nullable=False),
|
||||
sa.Column('skill_id', sa.Integer(), nullable=False),
|
||||
@ -108,6 +115,7 @@ def downgrade():
|
||||
op.drop_table('token')
|
||||
op.drop_table('user')
|
||||
op.drop_table('profile_skill')
|
||||
op.drop_table('profile_searchtopic')
|
||||
op.drop_table('profile_language')
|
||||
op.drop_table('contact')
|
||||
op.drop_table('address')
|
Reference in New Issue
Block a user