Compare commits

..

24 Commits

Author SHA1 Message Date
6389258a60 Add more skills
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-08-16 22:43:20 +02:00
b8c2644086 implement generic search
All checks were successful
continuous-integration/drone/push Build is passing
closes #47
2021-08-16 22:41:03 +02:00
ec855a542d add level texts, closes #45
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-14 12:14:01 +02:00
9a1f3f842a extend language name
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-02 21:51:44 +02:00
2d49c70f0f fix column type
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-02 21:29:06 +02:00
39f014db86 add pymysql
All checks were successful
continuous-integration/drone/push Build is passing
2021-08-02 19:57:19 +02:00
922b20c990 Merge pull request 'docker-compose lokal gebaute Images' (#43) from feature-docker-local into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #43
2021-08-02 18:02:26 +02:00
5a84ba0689 switch docker-compose to local build
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-28 22:28:34 +02:00
54f1c0f242 add integration docker-compose file
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-26 21:52:29 +02:00
178381b00c extend seed skills
All checks were successful
continuous-integration/drone/push Build is passing
2021-07-26 20:17:38 +02:00
730878847b Merge pull request 'Anmeldung per LDAP' (#35) from feature-ldap into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2021-07-26 19:44:19 +02:00
ea57f326b7 Merge pull request 'Profilsuche nach Nickname' (#41) from feature-nickname-search into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #41
2021-07-26 19:40:33 +02:00
76e95311f0 add search by nick
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
add sql special chars test
2021-07-26 19:31:29 +02:00
cd3f0f173b Merge pull request 'Fix SVG response header' (#40) from fix-icons into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #40
2021-07-12 21:16:49 +02:00
c37824785d fix svg icon header
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-12 20:18:45 +02:00
a1502fea9c fix tests
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-12 18:37:54 +02:00
5bd6c827d8 fix ldap3 version
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-07-11 12:19:41 +02:00
bbf8719fc9 add ldap auth
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-07-11 12:16:41 +02:00
cd2a7853dd Merge pull request 'searchtopics analog Skills ohne Level' (#34) from feature-searchtopics into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2021-07-06 21:22:19 +02:00
1b58c65666 fix migrations
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-06 21:19:58 +02:00
2f83e206e1 implement searchtopics 2021-07-06 21:18:47 +02:00
8e087198a4 Merge pull request 'Profilsuche' (#31) from feature-profile-search into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #31
2021-07-06 21:17:00 +02:00
85b8e638a7 add reuse headers to profile search files
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-07-06 21:14:41 +02:00
020edffec7 implement profile search
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2021-07-06 21:04:47 +02:00
22 changed files with 762 additions and 111 deletions

View File

@ -26,9 +26,6 @@ steps:
from_secret: "docker_username" from_secret: "docker_username"
password: password:
from_secret: "docker_password" from_secret: "docker_password"
when:
event:
- tag
image_pull_secrets: image_pull_secrets:
- dockerconfig - dockerconfig

View File

@ -16,6 +16,8 @@ sqlalchemy = "~=1.4.18"
waitress = "~=2.0.0" waitress = "~=2.0.0"
pyyaml = "~=5.4.1" pyyaml = "~=5.4.1"
flask-cors = "~=3.0.10" flask-cors = "~=3.0.10"
ldap3 = "~=2.9"
pymysql = "~=1.0.2"
[dev-packages] [dev-packages]
flake8 = "~=3.9.2" flake8 = "~=3.9.2"

162
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "128a3b90d2e3d5876c5942fb256dba2d5e3d0b3a545cae6d51aeda6925a0f593" "sha256": "332b04c923ecb74bc066ee5348a664274ce87736981cc8e56e60070ff26e7d0e"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -135,6 +135,17 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==3.0.1" "version": "==3.0.1"
}, },
"ldap3": {
"hashes": [
"sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6",
"sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687",
"sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70",
"sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5",
"sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"
],
"index": "pypi",
"version": "==2.9.1"
},
"mako": { "mako": {
"hashes": [ "hashes": [
"sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab", "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab",
@ -183,13 +194,39 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2.0.1" "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": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "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": { "python-dotenv": {
"hashes": [ "hashes": [
@ -254,39 +291,39 @@
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
"sha256:0f6d467b67a7e5048f1408e8ea60d6caa70be5b386d0eebbf1185ab49cb8c7e4", "sha256:09dbb4bc01a734ccddbf188deb2a69aede4b3c153a72b6d5c6900be7fb2945b1",
"sha256:238d78b3110b7f7cffdb70bf9cda686e0d876a849bc78ba4d471aa7b1461f306", "sha256:12bac5fa1a6ea870bdccb96fe01610641dd44ebe001ed91ef7fcd980e9702db5",
"sha256:25c0e0f3a7e8c19350086b3c0fe93c4def045cec053d749ef15da710c4d54c81", "sha256:1fdae7d980a2fa617d119d0dc13ecb5c23cc63a8b04ffcb5298f2c59d86851e9",
"sha256:2f60a2e599cf5cf5e5327ce60f2918b897e42ad9f405d10dd01e37869c0ce6fc", "sha256:26daa429f039e29b1e523bf763bfab17490556b974c77b5ca7acb545b9230e9a",
"sha256:38ee3a266afef2978e82824650457f70c5d74ec0cadec1b10fe5ed6f038eb5d0", "sha256:36a089dc604032d41343d86290ce85d4e6886012eea73faa88001260abf5ff81",
"sha256:46361690f1e1c5385994a4caeb6e8126063ff593a5c635700bbc1245de793c1e", "sha256:39b5d36ab71f73c068cdcf70c38075511de73616e6c7fdd112d6268c2704d9f5",
"sha256:46b99eab618cdc1c871ea707b7c52edc23cfea6c750740cd242ba62b5c84de7f", "sha256:4014978de28163cd8027434916a92d0f5bb1a3a38dff5e8bf8bff4d9372a9117",
"sha256:4a67371752fd86d1d03a3b82d4e75404608f6f4d579b9676124079a22a40c79f", "sha256:44d23ea797a5e0be71bc5454b9ae99158ea0edc79e2393c6e9a2354de88329c0",
"sha256:525dd3c2205b11a2bc6d770bf1ec63bde0253fd754b4c19c399d27ddc9dad0d3", "sha256:488608953385d6c127d2dcbc4b11f8d7f2f30b89f6bd27c01b042253d985cc2f",
"sha256:6c8406c3d8c1c7d15da454de15d77f7bb48d14ede5db994f74226c348cf1050e", "sha256:5102b9face693e8b2db3b2539c7e1a5d9a5b4dc0d79967670626ffd2f710d6e6",
"sha256:6da83225a23eaf7b3f48f3d5f53c91b2cf00fbfa48b24a7a758160112dd3e123", "sha256:5908ea6c652a050d768580d01219c98c071e71910ab8e7b42c02af4010608397",
"sha256:7150e5b543b466f45f668b352f7abda27998cc8035f051d1b7e9524ca9eb2f5f", "sha256:5d856cc50fd26fc8dd04892ed5a5a3d7eeb914fea2c2e484183e2d84c14926e0",
"sha256:76fbc24311a3d039d6cd147d396719f606d96d1413f3816c028a48e29367f646", "sha256:68393d3fd31469845b6ba11f5b4209edbea0b58506be0e077aafbf9aa2e21e11",
"sha256:854a7b15750e617e16f8d65dbc004f065a7963544b253b923f16109557648777", "sha256:6a16c7c4452293da5143afa3056680db2d187b380b3ef4d470d4e29885720de3",
"sha256:86c079732328f1add097b0b8079cd532b5d28e207fac93e9d6ea5f487506deef", "sha256:756f5d2f5b92d27450167247fb574b09c4cd192a3f8c2e493b3e518a204ee543",
"sha256:8d860c62e3f51623ccd528d8fac44580501df557d4b467cc5581587fcf057719", "sha256:891927a49b2363a4199763a9d436d97b0b42c65922a4ea09025600b81a00d17e",
"sha256:9675d5bc7e4f96a7bb2b54d14e9b269a5fb6e5d36ecc7d01f0f65bb9af3185f9", "sha256:9bfe882d5a1bbde0245dca0bd48da0976bd6634cf2041d2fdf0417c5463e40e5",
"sha256:9841762d114018c49483c089fa2d47f7e612e57666323f615913d7d7f46e9606", "sha256:9fcbb4b4756b250ed19adc5e28c005b8ed56fdb5c21efa24c6822c0575b4964d",
"sha256:9eb25bcf9161e2fcbe9eebe8e829719b2334e849183f0e496bf4b83722bcccfa", "sha256:a00d9c6d3a8afe1d1681cd8a5266d2f0ed684b0b44bada2ca82403b9e8b25d39",
"sha256:aad3234a41340e9cf6184e621694e2a7233ba3f8aef9b1e6de8cba431b45ebd2", "sha256:a5e14cb0c0a4ac095395f24575a0e7ab5d1be27f5f9347f1762f21505e3ba9f1",
"sha256:b502b5e2f08500cc4b8d29bfc4f51d805adcbc00f8d149e98fda8aae85ddb644", "sha256:b48148ceedfb55f764562e04c00539bb9ea72bf07820ca15a594a9a049ff6b0e",
"sha256:b86d83fefc8a8c394f3490c37e1953bc16c311a3d1d1cf91518793bfb9847fb4", "sha256:b7fb937c720847879c7402fe300cfdb2aeff22349fa4ea3651bca4e2d6555939",
"sha256:c0eb2cd3ad4967fcbdd9e066e8cd91fe2c23c671dbae9952f0b4d3d42832cc5f", "sha256:bc34a007e604091ca3a4a057525efc4cefd2b7fe970f44d20b9cfa109ab1bddb",
"sha256:e0d48456e1aa4f0537f9c9af7be71e1f0659ff68bc1cd538ebc785f6b007bd0d", "sha256:c9373ef67a127799027091fa53449125351a8c943ddaa97bec4e99271dbb21f4",
"sha256:eaee5dd378f6f0d7c3ec49aeeb26564d55ac0ad73b9b4688bf29e66deabddf73", "sha256:d09a760b0a045b4d799102ae7965b5491ccf102123f14b2a8cc6c01d1021a2d9",
"sha256:f14acb0fd16d404fda9370f93aace682f284340c89c3442ac747c5466ac7e2b5", "sha256:ec1be26cdccd60d180359a527d5980d959a26269a2c7b1b327a1eea0cab37ed8",
"sha256:f6fc526bd70898489d02bf52c8f0632ab377592ae954d0c0a5bb38d618dddaa9", "sha256:eedd76f135461cf237534a6dc0d1e0f6bb88a1dc193678fab48a11d223462da5",
"sha256:fcd84e4d46a86291495d131a7824ba38d2e8278bda9425c50661a04633174319", "sha256:f028ef6a1d828bc754852a022b2160e036202ac8658a6c7d34875aafd14a9a15",
"sha256:ff38ecf89c69a531a7326c2dae71982edfe2f805f3c016cdc5bfd1a04ebf80cb", "sha256:f814d80844969b0d22ea63663da4de5ca1c434cfbae226188901e5d368792c17",
"sha256:ff8bebc7a9d297dff2003460e01db2c20c63818b45fb19170f388b1a72fe5a14" "sha256:fd2102a8f8a659522719ed73865dff3d3cc76eb0833039dc473e0ad3041d04be"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.4.20" "version": "==1.4.22"
}, },
"waitress": { "waitress": {
"hashes": [ "hashes": [
@ -306,12 +343,13 @@
} }
}, },
"develop": { "develop": {
"appdirs": { "backports.entry-points-selectable": {
"hashes": [ "hashes": [
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"
], ],
"version": "==1.4.4" "markers": "python_version >= '2.7'",
"version": "==1.1.0"
}, },
"binaryornot": { "binaryornot": {
"hashes": [ "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'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"charset-normalizer": {
"hashes": [
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
],
"markers": "python_version >= '3'",
"version": "==2.0.4"
},
"distlib": { "distlib": {
"hashes": [ "hashes": [
"sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736", "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736",
@ -374,19 +420,19 @@
}, },
"identify": { "identify": {
"hashes": [ "hashes": [
"sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421", "sha256:7abaecbb414e385752e8ce02d8c494f4fbc780c975074b46172598a28f1ab839",
"sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306" "sha256:a0e700637abcbd1caae58e0463861250095dfe330a8371733a471af706a4a29a"
], ],
"markers": "python_full_version >= '3.6.1'", "markers": "python_full_version >= '3.6.1'",
"version": "==2.2.10" "version": "==2.2.11"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '3'",
"version": "==2.10" "version": "==3.2"
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
@ -457,6 +503,14 @@
], ],
"version": "==1.6.0" "version": "==1.6.0"
}, },
"platformdirs": {
"hashes": [
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
],
"markers": "python_version >= '3.6'",
"version": "==2.2.0"
},
"pre-commit": { "pre-commit": {
"hashes": [ "hashes": [
"sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378", "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378",
@ -526,11 +580,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.25.1" "version": "==2.26.0"
}, },
"reuse": { "reuse": {
"hashes": [ "hashes": [
@ -566,11 +620,11 @@
}, },
"virtualenv": { "virtualenv": {
"hashes": [ "hashes": [
"sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467", "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094",
"sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76" "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.4.7" "version": "==20.7.0"
}, },
"yapf": { "yapf": {
"hashes": [ "hashes": [

View File

@ -68,9 +68,18 @@ flake8
### Testbenutzer ### Testbenutzer
#### Lokal ohne LDAP
Für ein Login ohne LDAP werden die Benutzer aus der [`auth.yml`](./data/auth.yml) benutzt. 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 ### Beispiel-Requests
Beispiele brauchen curl und jq. Beispiele brauchen curl und jq.
@ -113,6 +122,15 @@ curl -s \
http://localhost:5000/users/1/profile 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 ## 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. [`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 ## Lizenzen
Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation. Dieses Projekt erfüllt die [REUSE](https://reuse.software/) Spezifikation.

19
app.py
View File

@ -8,25 +8,36 @@ import os
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy from flask.logging import default_handler
from flask_migrate import Migrate 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()) load_dotenv(find_dotenv())
app = Flask(__name__)
loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING) loglevel = os.getenv("KI_LOGLEVEL", logging.WARNING)
loglevel = int(loglevel) loglevel = int(loglevel)
app.logger.setLevel(loglevel)
logging.basicConfig(level=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_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["KI_DATA_DIR"] = os.path.dirname(__file__) + "/data" 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["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) CORS(app)
db = SQLAlchemy(app) db = SQLAlchemy(app)
migrate = Migrate(app, db) migrate = Migrate(app, db)
logging.debug("Hello from KI")
from ki import module # noqa from ki import module # noqa

View File

@ -7,3 +7,5 @@ peter:
password: geheim password: geheim
klaus: klaus:
password: jutta password: jutta
dieter:
password: hunger

View File

@ -11,3 +11,116 @@ id,name
10,PostgreSQL 10,PostgreSQL
11,SQLite 11,SQLite
12,Node.js 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

1 id name
11 10 PostgreSQL
12 11 SQLite
13 12 Node.js
14 13 C++
15 14 C
16 15 VHDL
17 16 go
18 17 Perl
19 18 3D-Druck
20 19 ABAP
21 20 Android
22 21 Ansible
23 22 Arduino
24 23 Bash
25 24 batou
26 25 bind
27 26 Buchführung
28 27 C#
29 28 CAD
30 29 CAM
31 30 Cobol
32 31 CRM
33 32 CSS
34 33 D
35 34 Debian
36 35 Delphi
37 36 DevOPS
38 37 Discourse
39 38 Django
40 39 DNS
41 40 Dovecot
42 41 Elasticsearch
43 42 Emacs
44 43 Email
45 44 ERP
46 45 ESP
47 46 Excel
48 47 Fahrdienstleitung
49 48 Fedora
50 49 FLOSS
51 50 Geographie
52 51 Geologie
53 52 Gnome
54 53 GPS
55 54 Grafana
56 55 GrayLog
57 56 GSM
58 57 GTK
59 58 HTML
60 59 IBM Z
61 60 IMAP
62 61 Ionic
63 62 iOS
64 63 Java
65 64 Kryptographie
66 65 LDAP
67 66 LibreOffice
68 67 Linux
69 68 Literaturgeschichte
70 69 Lithographie
71 70 Lucene
72 71 Mailman
73 72 MariaDB
74 73 Markdown
75 74 Marketing
76 75 Microsoft Office
77 76 Monitoring
78 77 Nagios
79 78 nähen
80 79 NixOS
81 80 odoo
82 81 OpenOffice
83 82 OpenPGP
84 83 OpenStreetMap
85 84 openSUSE
86 85 Oracle
87 86 Percona
88 87 PGP
89 88 Plone
90 89 Postfix
91 90 PowerDNS
92 91 PowerPC
93 92 Projektmanagement
94 93 puppet
95 94 Qt
96 95 R/3
97 96 RedHat
98 97 RHEL
99 98 S/4
100 99 Salt
101 100 SAP
102 101 Scrum
103 102 ScummVM
104 103 Sensu
105 104 sh
106 105 Siemens S5
107 106 Siemens S7
108 107 Simatic
109 108 SLES
110 109 Spring
111 110 SQL
112 111 Stenographie
113 112 Steuerrecht
114 113 Teppich knüpfen
115 114 TeX
116 115 Verlagswesen
117 116 vi(m)
118 117 Windows 10
119 118 Windows 2000
120 119 Windows 2012R2
121 120 Windows 7
122 121 Windows NT
123 122 Windows XP
124 123 x86
125 124 Zope
126 125 zsh

22
docker-compose.yml Normal file
View 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"

View File

@ -9,7 +9,12 @@ CORS_ORIGINS=*
FLASK_APP=app.py FLASK_APP=app.py
FLASK_ENV=development FLASK_ENV=development
# auth method: file or ldap
KI_AUTH=file KI_AUTH=file
# ldap auth only
KI_LDAP_URL=ldap://localhost:10389
KI_LDAP_ROOT_DN=dc=example,dc=com
# 10 = debug # 10 = debug
KI_LOGLEVEL=10 KI_LOGLEVEL=10

View File

@ -6,7 +6,8 @@ import csv
import logging import logging
from app import app, db 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(): def seed_contacttypes():
@ -25,12 +26,37 @@ def seed_contacttypes():
db.session.add(ContactType(id=int(contacttype["id"]), name=contacttype["name"])) 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): def seed(dev: bool):
seed_contacttypes() seed_contacttypes()
skill_seed_file_path = app.config["KI_DATA_DIR"] + "/seed_data/skills.csv" 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: with open(skill_seed_file_path) as skills_file:
skills_csv_reader = csv.DictReader(skills_file) skills_csv_reader = csv.DictReader(skills_file)
@ -42,7 +68,7 @@ def seed(dev: bool):
if db_skill is None: if db_skill is None:
db.session.add(Skill(id=int(skill["id"]), name=skill["name"])) 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" 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"])) db.session.add(Language(id=iso["639-1"], name=iso["Sprache"]))
if dev: if dev:
logging.info("seeding peter :)") app.logger.info("seeding peter :)")
peter = User(auth_id="peter") peter = User(auth_id="peter")
db.session.add(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) peters_php_skill = ProfileSkill(profile=peters_profile, skill_id=1, level=5)
db.session.add(peters_php_skill) 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) peter_de = ProfileLanguage(profile=peters_profile, language_id="de", level=5)
db.session.add(peter_de) db.session.add(peter_de)
peter_fr = ProfileLanguage(profile=peters_profile, language_id="fr", level=3) peter_fr = ProfileLanguage(profile=peters_profile, language_id="fr", level=3)
db.session.add(peter_fr) db.session.add(peter_fr)
logging.info("seeding klaus :D") seed_user("klaus")
klaus = User(auth_id="klaus") seed_user("dirtydieter",
db.session.add(klaus) 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() db.session.commit()

View File

@ -5,11 +5,28 @@
import uuid import uuid
import yaml import yaml
from ldap3 import Server, Connection, ALL
from app import app, db from app import app, db
from ki.models import User, Token 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" auth_file_path = app.config["KI_DATA_DIR"] + "/auth.yml"
with open(auth_file_path, "r") as auth_file_stream: with open(auth_file_path, "r") as auth_file_stream:
@ -23,14 +40,32 @@ def auth(username, password):
if auth_user["password"] != password: if auth_user["password"] != password:
return None 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) def ldap_auth(username, password):
db.session.add(token) app.logger.debug("performing LDAP authentication")
db.session.commit()
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')

View File

@ -2,4 +2,5 @@
# #
# SPDX-License-Identifier: AGPL-3.0-or-later # 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 from ki.handlers.update_profile import update_profile # noqa

View 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})

View File

@ -5,7 +5,8 @@
from flask import make_response, request from flask import make_response, request
from sqlalchemy import not_ 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 from app import db
@ -71,6 +72,30 @@ def update_skills(profile, skills_data):
not_(ProfileSkill.skill_id.in_(profile_skill_ids))).delete() 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): def update_contacts(profile, contacts_data):
contact_ids_to_be_deleted = list(map(lambda c: c.id, profile.contacts)) 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_address(profile, request.json.get("address", {}))
update_contacts(profile, request.json.get("contacts", {})) update_contacts(profile, request.json.get("contacts", {}))
update_skills(profile, request.json.get("skills", {})) update_skills(profile, request.json.get("skills", {}))
update_searchtopics(profile, request.json.get("searchtopics"))
update_languages(profile, request.json.get("languages", {})) update_languages(profile, request.json.get("languages", {}))
db.session.commit() db.session.commit()

View File

@ -41,6 +41,7 @@ class Profile(db.Model):
contacts = relationship("Contact") contacts = relationship("Contact")
address = relationship("Address", uselist=False, back_populates="profile") address = relationship("Address", uselist=False, back_populates="profile")
skills = relationship("ProfileSkill", back_populates="profile") skills = relationship("ProfileSkill", back_populates="profile")
searchtopics = relationship("ProfileSearchtopic", back_populates="profile")
languages = relationship("ProfileLanguage", back_populates="profile") languages = relationship("ProfileLanguage", back_populates="profile")
def to_dict(self): def to_dict(self):
@ -52,9 +53,10 @@ class Profile(db.Model):
"availability": self.availability, "availability": self.availability,
"freetext": self.freetext, "freetext": self.freetext,
"visible": self.visible, "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)), "contacts": list(map(lambda contact: contact.to_dict(), self.contacts)),
"skills": list(map(lambda skill: skill.to_dict(), self.skills)), "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)) "languages": list(map(lambda language: language.to_dict(), self.languages))
} }
@ -131,18 +133,39 @@ class Address(db.Model):
class Skill(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" __tablename__ = "skill"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String(25), unique=True, nullable=False) name = Column(String(25), unique=True, nullable=False)
profiles = relationship("ProfileSkill", back_populates="skill") profiles = relationship("ProfileSkill", back_populates="skill")
searchtopics = relationship("ProfileSearchtopic", back_populates="skill")
def to_dict(self): def to_dict(self):
return {"id": self.id, "name": self.name, "icon_url": "/skills/{}/icon".format(self.id)} return {"id": self.id, "name": self.name, "icon_url": "/skills/{}/icon".format(self.id)}
class ProfileSkill(db.Model): 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" __tablename__ = "profile_skill"
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True) 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} 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): class Language(db.Model):
__tablename__ = "language" __tablename__ = "language"
id = Column(String(2), primary_key=True) id = Column(String(4), primary_key=True)
name = Column(String(25), nullable=False) name = Column(String(100), nullable=False)
profiles = relationship("ProfileLanguage", back_populates="language") profiles = relationship("ProfileLanguage", back_populates="language")
@ -169,10 +205,23 @@ class Language(db.Model):
class ProfileLanguage(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" __tablename__ = "profile_language"
profile_id = Column(Integer, ForeignKey("profile.id"), primary_key=True) 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) level = Column(SmallInteger, nullable=False)
profile = relationship("Profile", back_populates="languages") profile = relationship("Profile", back_populates="languages")

View File

@ -7,10 +7,14 @@ from flask import g, make_response, request, send_file
from functools import wraps from functools import wraps
from ki.auth import auth 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.handlers import update_profile as update_profile_handler
from ki.models import ContactType, Language, Skill, Token, User from ki.models import ContactType, Language, Skill, Token, User
from app import app from app import app
content_type_svg = "image/svg+xml"
content_type_png = "image/png"
def token_auth(func): def token_auth(func):
@wraps(func) @wraps(func)
@ -70,22 +74,22 @@ def handle_icon_request(model, id, path):
icon_svg_path = icon_base_path + ".svg" icon_svg_path = icon_base_path + ".svg"
if os.path.exists(icon_svg_path): 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" icon_png_path = icon_base_path + ".png"
if os.path.exists(icon_png_path): 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" unknown_svg_path = path + "unknown.svg"
if os.path.exists(unknown_svg_path): 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" unknown_png_path = path + "unknown.png"
if os.path.exists(unknown_png_path): 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) return make_response({"error": "icon not found"}, 404)
@ -143,6 +147,12 @@ def get_contacttypes():
return handle_completion_request(ContactType, "contacttypes") return handle_completion_request(ContactType, "contacttypes")
@app.route("/users/profiles")
@token_auth
def find_profiles():
return find_profiles_handler()
@app.route("/skills") @app.route("/skills")
@token_auth @token_auth
def get_skills(): def get_skills():

View File

@ -8,6 +8,7 @@ import unittest
from app import app, db, migrate from app import app, db, migrate
from ki.actions import seed from ki.actions import seed
from ki.models import Skill
class ApiTest(unittest.TestCase): class ApiTest(unittest.TestCase):
@ -15,8 +16,10 @@ class ApiTest(unittest.TestCase):
def setUp(self): def setUp(self):
app.debug = True app.debug = True
app.config["KI_AUTH"] = "file"
app.config["TESTING"] = True app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
self.client = app.test_client() self.client = app.test_client()
with app.app_context(): with app.app_context():
@ -24,6 +27,8 @@ class ApiTest(unittest.TestCase):
command.upgrade(config, "head") command.upgrade(config, "head")
seed(True) seed(True)
max_skill = Skill.query.order_by(Skill.id.desc()).first()
self.max_skill_id = max_skill.id
def tearDown(self): def tearDown(self):
db.drop_all() db.drop_all()

View 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()

View 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()

View File

@ -71,6 +71,19 @@ class TestProfileEndpoint(ApiTest):
}, },
"level": 5 "level": 5
}], }],
"searchtopics": [{
"profile_id": 1,
"skill": {
"id": 3,
"name": "Python",
"icon_url": "/skills/3/icon"
}
}, {
"profile_id": 1,
"skill": {
"name": "Assembler"
}
}],
"languages": [{ "languages": [{
"id": 1, "id": 1,
"language": { "language": {
@ -130,10 +143,21 @@ class TestProfileEndpoint(ApiTest):
self.assertEqual(first_skill.level, 4) self.assertEqual(first_skill.level, 4)
second_skill = skills[1] 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.skill.name, "Tschunkproduktion")
self.assertEqual(second_skill.level, 5) 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 languages = profile.languages
self.assertEqual(len(languages), 2) self.assertEqual(len(languages), 2)
@ -150,7 +174,14 @@ class TestProfileEndpoint(ApiTest):
response = self.client.get("/users/1/profile") response = self.client.get("/users/1/profile")
self.assertEqual(response.status_code, 401) 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_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")
@ -216,6 +247,21 @@ class TestProfileEndpoint(ApiTest):
}, },
"level": 3 "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": [{ "languages": [{
"profile_id": 1, "profile_id": 1,
"language": { "language": {

View File

@ -14,30 +14,39 @@ class TestSkillsEndpoint(ApiTest):
self.assertIn("Access-Control-Allow-Origin", response.headers) self.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*") 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"] token = self.login("peter", "geheim")["token"]
response = self.client.get("/skills?search=p", headers={"Authorization": "Bearer " + token}) response = self.client.get("/skills?search=p", headers={"Authorization": "Bearer " + token})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual( self.assertGreaterEqual(len(response.json['skills']), 3)
{
"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.assertIn("Access-Control-Allow-Origin", response.headers)
self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*") 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": if __name__ == "main":
unittest.main() unittest.main()

View File

@ -1,8 +1,8 @@
"""Initial migration. """Initial migration.
Revision ID: 808fe55111df Revision ID: 9183e2335b05
Revises: Revises:
Create Date: 2021-07-05 20:13:50.560579 Create Date: 2021-08-02 21:51:30.400680
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '808fe55111df' revision = '9183e2335b05'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -24,8 +24,8 @@ def upgrade():
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('language', op.create_table('language',
sa.Column('id', sa.String(length=2), nullable=False), sa.Column('id', sa.String(length=4), nullable=False),
sa.Column('name', sa.String(length=25), nullable=False), sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('profile', op.create_table('profile',
@ -71,12 +71,19 @@ def upgrade():
) )
op.create_table('profile_language', op.create_table('profile_language',
sa.Column('profile_id', sa.Integer(), nullable=False), 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.Column('level', sa.SmallInteger(), nullable=False),
sa.ForeignKeyConstraint(['language_id'], ['language.id'], ), sa.ForeignKeyConstraint(['language_id'], ['language.id'], ),
sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ), sa.ForeignKeyConstraint(['profile_id'], ['profile.id'], ),
sa.PrimaryKeyConstraint('profile_id', 'language_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', op.create_table('profile_skill',
sa.Column('profile_id', sa.Integer(), nullable=False), sa.Column('profile_id', sa.Integer(), nullable=False),
sa.Column('skill_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('token')
op.drop_table('user') op.drop_table('user')
op.drop_table('profile_skill') op.drop_table('profile_skill')
op.drop_table('profile_searchtopic')
op.drop_table('profile_language') op.drop_table('profile_language')
op.drop_table('contact') op.drop_table('contact')
op.drop_table('address') op.drop_table('address')