Merged in feature/new-hep-api (pull request #85)

Feature/new hep api

Approved-by: Christian Cueni
This commit is contained in:
Christian Cueni 2021-07-19 18:54:58 +00:00
commit 7747a039cb
106 changed files with 41599 additions and 16459 deletions

View File

@ -44,3 +44,4 @@ unittest-xml-reporting = "*"
django-silk = "*" django-silk = "*"
wagtail-autocomplete = "*" wagtail-autocomplete = "*"
jedi = "==0.17.2" jedi = "==0.17.2"
Authlib = "*"

575
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "58d8faf7e03679ac7b0053dd01e54288d3a719c8ee25c1edf20a74ebcbf87951" "sha256": "37e4b67556de5b9daa800e1078361cffdca6044d49ab74132616b10570a57acb"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -23,6 +23,22 @@
], ],
"version": "==7.0.0" "version": "==7.0.0"
}, },
"appnope": {
"hashes": [
"sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442",
"sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.2"
},
"authlib": {
"hashes": [
"sha256:37df3a2554bc6fe0da3cc6848c44fac2ae40634a7f8fc72543947f4330b26464",
"sha256:d9fe5edb59801b16583faa86f88d798d99d952979b9616d5c735b9170b41ae2c"
],
"index": "pypi",
"version": "==0.15.4"
},
"autopep8": { "autopep8": {
"hashes": [ "hashes": [
"sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0", "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0",
@ -55,42 +71,110 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:56f1766f1271b6b4e979c7b56225377f8912050e5935adc5c1c9e3a0338b949e", "sha256:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31",
"sha256:c61c809d288e88b9a0d926f56f803d0128b498aa9b45a42a6e03cd9a83e5c124" "sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.68" "version": "==1.17.98"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:0f693f5ad6348ec1a62b3a66fee2840d3b722d66b44896022d644275ff8b143d", "sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f",
"sha256:eb3544911cb0316a33b328a27d137130af278a9c0006be0c95e5e402b01d9865" "sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.20.98"
"version": "==1.20.68"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
], ],
"version": "==2020.12.5" "version": "==2021.5.30"
},
"cffi": {
"hashes": [
"sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813",
"sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373",
"sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69",
"sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f",
"sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06",
"sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05",
"sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea",
"sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee",
"sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0",
"sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396",
"sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7",
"sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f",
"sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73",
"sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315",
"sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76",
"sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1",
"sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49",
"sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed",
"sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892",
"sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482",
"sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058",
"sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5",
"sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53",
"sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045",
"sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3",
"sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55",
"sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5",
"sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e",
"sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c",
"sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369",
"sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827",
"sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053",
"sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa",
"sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4",
"sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322",
"sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132",
"sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62",
"sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa",
"sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0",
"sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396",
"sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e",
"sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991",
"sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6",
"sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc",
"sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1",
"sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406",
"sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333",
"sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d",
"sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
],
"version": "==1.14.5"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"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"
}, },
"cryptography": {
"hashes": [
"sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
"sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
"sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
"sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
"sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
"sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
"sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
"sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
"sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
"sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
"sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
"sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
],
"version": "==3.4.7"
},
"decorator": { "decorator": {
"hashes": [ "hashes": [
"sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060", "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323",
"sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98" "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"
], ],
"markers": "python_version >= '3.5'", "version": "==5.0.9"
"version": "==5.0.7"
}, },
"dj-database-url": { "dj-database-url": {
"hashes": [ "hashes": [
@ -101,11 +185,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:db2214db1c99017cbd971e58824e6f424375154fe358afc30e976f5b99fc6060", "sha256:3339ff0e03dee13045aef6ae7b523edff75b6d726adf7a7a48f53d5a501f7db7",
"sha256:e831105edb153af1324de44d06091ca75520a227456387dda4a47d2f1cc2731a" "sha256:f2084ceecff86b1e631c2cd4107d435daf4e12f1efcdf11061a73bf0b5e95f92"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.22" "version": "==2.2.24"
}, },
"django-appconf": { "django-appconf": {
"hashes": [ "hashes": [
@ -189,7 +273,6 @@
"sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8", "sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8",
"sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac" "sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.24.0" "version": "==0.24.0"
}, },
"django-treebeard": { "django-treebeard": {
@ -197,7 +280,6 @@
"sha256:7c2b1cdb1e9b46d595825186064a1228bc4d00dbbc186db5b0b9412357fba91c", "sha256:7c2b1cdb1e9b46d595825186064a1228bc4d00dbbc186db5b0b9412357fba91c",
"sha256:80150017725239702054e5fa64dc66e383dc13ac262c8d47ee5a82cb005969da" "sha256:80150017725239702054e5fa64dc66e383dc13ac262c8d47ee5a82cb005969da"
], ],
"markers": "python_version >= '3.6'",
"version": "==4.5.1" "version": "==4.5.1"
}, },
"djangorestframework": { "djangorestframework": {
@ -225,17 +307,15 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:156854f36d4086bb21ff85a79b4d6a6403a240cd2c17a33a44b8ea4ff4e957c2", "sha256:ccd76cd86a49f1042811faaa3a7d1b094fcf8e60a1ec286190417bbb5a3f2f76",
"sha256:a2ed065342e91a7672407325848cd5728d5e5eb4928d0a1c478fd4f0dd97d1f7" "sha256:cda50f6afaa4075464d7500ac838ec3cac3cc6824297e4340b2a17a62dc086a8"
], ],
"markers": "python_version >= '3.6'", "version": "==8.8.1"
"version": "==8.1.2"
}, },
"future": { "future": {
"hashes": [ "hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.18.2" "version": "==0.18.2"
}, },
"gprof2dot": { "gprof2dot": {
@ -286,7 +366,6 @@
"sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d",
"sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.1" "version": "==1.1"
}, },
"idna": { "idna": {
@ -294,16 +373,15 @@
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10" "version": "==2.10"
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:714810a5c74f512b69d5f3b944c86e592cee0a5fb9c728e582f074610f6cf038", "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64",
"sha256:f78c6a3972dde1cc9e4041cbf4de583546314ba52d3c97208e5b6b2221a9cb7d" "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf"
], ],
"index": "pypi", "index": "pypi",
"version": "==7.23.1" "version": "==7.16.1"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@ -322,127 +400,90 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.0.1"
"version": "==2.11.3"
}, },
"jmespath": { "jmespath": {
"hashes": [ "hashes": [
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0" "version": "==0.10.0"
}, },
"libsass": { "libsass": {
"hashes": [ "hashes": [
"sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b", "sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb",
"sha256:1b2d415bbf6fa7da33ef46e549db1418498267b459978eff8357e5e823962d35", "sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529",
"sha256:25ebc2085f5eee574761ccc8d9cd29a9b436fc970546d5ef08c6fa41eb57dff1", "sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613",
"sha256:2ae806427b28bc1bb7cb0258666d854fcf92ba52a04656b0b17ba5e190fb48a9", "sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e",
"sha256:4a246e4b88fd279abef8b669206228c92534d96ddcd0770d7012088c408dff23", "sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7",
"sha256:553e5096414a8d4fb48d0a48f5a038d3411abe254d79deac5e008516c019e63a", "sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb",
"sha256:697f0f9fa8a1367ca9ec6869437cb235b1c537fc8519983d1d890178614a8903", "sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a",
"sha256:a8fd4af9f853e8bf42b1425c5e48dd90b504fa2e70d7dac5ac80b8c0a5a5fe85", "sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2",
"sha256:c9411fec76f480ffbacc97d8188322e02a5abca6fc78e70b86a2a2b421eae8a2", "sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6"
"sha256:daa98a51086d92aa7e9c8871cf1a8258124b90e2abf4697852a3dca619838618",
"sha256:e0e60836eccbf2d9e24ec978a805cd6642fa92515fbd95e3493fee276af76f8a",
"sha256:e64ae2587f1a683e831409aad03ba547c245ef997e1329fffadf7a866d2510b8",
"sha256:f6852828e9e104d2ce0358b73c550d26dd86cc3a69439438c3b618811b9584f5"
], ],
"version": "==0.20.1" "version": "==0.21.0"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.0.1"
"version": "==1.1.1"
},
"matplotlib-inline": {
"hashes": [
"sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811",
"sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"
],
"markers": "python_version >= '3.5'",
"version": "==0.1.2"
}, },
"newrelic": { "newrelic": {
"hashes": [ "hashes": [
"sha256:242a5e901d684f7ffdd621bc58da8fe9a85d5545b4b63e1070589f5ab45c9e1e", "sha256:08a518af90505750ef2687411c0de7db438cc7b29e6d3855b27a7aae068b67b8",
"sha256:3dec4647de67609570c4e305f2b6432a00e0a0940a7ac69660ee92268b49d6e7", "sha256:6244e4fff580157e5ab7f7879d188f8bc6c8760b180a46625ba00c8f99f56e2a",
"sha256:489e5a450aae1a5ecf7ca488739bd274296b19049bb7927c7ef71953ad5ad437", "sha256:8a42ed37ba95dfc1a82bd70de6d8311d549fa2bad45229a98c24f33ef50e3616",
"sha256:503e5dfcbf215fe68e4349ea452b5b00234010122ca72d80b64c73270654916c", "sha256:9ee632b9418ae61a64523223e621004041bbd5be7c80d8b01d46a66f4989f213",
"sha256:5b0a04f7acf4dafe8d3935ac8688143bc0a0c61e15e2a779b152afcc3c88ee45", "sha256:beac3552c6e78de4a99320937c0e675fb3cc5cbe24ab7156964695312841ddc6",
"sha256:6a87cd6102aba7c9619a6e4b9e1aa6322ef81367b1a8f24ad996a07333313c8c", "sha256:c1575705d0f2c19cbef002504255e5da0ca5905dc5df02da71ebcb4e032de361",
"sha256:90d2bab0a08001d84499bf11c62c49d1fc6f2835c05d12994b5a931cad48f120", "sha256:c8fa74b8dba90e07dfa72e2b88e27b41091964c9843d9f411cb66c59cad90994",
"sha256:a3b928a052be318cb0cdb56977c630f1ded1d8e391c876ed5ae4442aa7ad499a", "sha256:ea43ee0cd3e19de074e17c47029f3965b486587fe95dfcc2ea4be1b7d0adb58c"
"sha256:adc748f633bd64e295b403448daa8416961b0f99af6787b857009d737c5e8af3",
"sha256:e767af29572a9457a5e5f13481fe735c1a9ae2d1683b7b35c8757f9df275a538",
"sha256:fede816248d0a1e5e11487ecc122f24c9d33e08a6ac1f882044ac6f3b2c90ae0"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.0.156" "version": "==6.4.2.159"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.9" "version": "==20.9"
}, },
"parso": { "parso": {
@ -450,7 +491,6 @@
"sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea",
"sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.7.1" "version": "==0.7.1"
}, },
"pexpect": { "pexpect": {
@ -473,30 +513,19 @@
"sha256:0013f590a8f260df60bcfd65db19d18efc04e7f046c3c82a40e2e2b3292a937c", "sha256:0013f590a8f260df60bcfd65db19d18efc04e7f046c3c82a40e2e2b3292a937c",
"sha256:0b899ee80920bb533f26581af9b4660bc12aff4562555afe74e429101ebf3c94", "sha256:0b899ee80920bb533f26581af9b4660bc12aff4562555afe74e429101ebf3c94",
"sha256:12f29d6c23424f704c66b5b68c02fe0b571504459605cfe36ab8158359b0e1bb", "sha256:12f29d6c23424f704c66b5b68c02fe0b571504459605cfe36ab8158359b0e1bb",
"sha256:135e9aa65150c53f7db85bf2bebb8a0e1a48ea850e80cf66e16dd04fa09d309c",
"sha256:153ec6f18f7b61641e0e6e502acfaf4a06c9aba2ea11c0b4b3578ea9f13a4a4a", "sha256:153ec6f18f7b61641e0e6e502acfaf4a06c9aba2ea11c0b4b3578ea9f13a4a4a",
"sha256:17fe25efc785194d48c38fad85dce470013ba19d2fb66639e149f14bccf1327f",
"sha256:1912b7230459fd53682dae32b83cbd8e5d642ba36d4be18566f00a9c063aa13d", "sha256:1912b7230459fd53682dae32b83cbd8e5d642ba36d4be18566f00a9c063aa13d",
"sha256:1a5b93084e01328a1cb1ecdad99d11d75e881e89a95f88d85b523646553b36c2", "sha256:1a5b93084e01328a1cb1ecdad99d11d75e881e89a95f88d85b523646553b36c2",
"sha256:25193f934d37d836a6b1f4c062ce574a96cbca7c6d9dc8ddfbbac7f9c54deaa4", "sha256:25193f934d37d836a6b1f4c062ce574a96cbca7c6d9dc8ddfbbac7f9c54deaa4",
"sha256:2c042352b430d678db50c78c5214e19638eff8b688941271da2de21fd298dfe5",
"sha256:2e818dbe445e86fc6c266973fe540c35125c42eb2cf13a6095e9adaa89c0deb5",
"sha256:2fcde9954c8882d1c7f93bb828caa34a4c5e3ee69dbc7895dc8652ad972b455a", "sha256:2fcde9954c8882d1c7f93bb828caa34a4c5e3ee69dbc7895dc8652ad972b455a",
"sha256:35f7d998b8e82fb3fb51ff88b30485eb81cd7dd56ec7e1a8deba23eb88532d44", "sha256:35f7d998b8e82fb3fb51ff88b30485eb81cd7dd56ec7e1a8deba23eb88532d44",
"sha256:37cc0339abfa9e295c75d9a7f227d35cb44716feb95057f9449c4a9e9a17daf7",
"sha256:43334f9581cd067945b8898cef9eb5714ee4883f8de0304c011f1dbdb1d4e2aa", "sha256:43334f9581cd067945b8898cef9eb5714ee4883f8de0304c011f1dbdb1d4e2aa",
"sha256:4bd4a71501b6d51db4abc07e1f43f5a6fed0a1a9583cca0b401d6af50284b0db", "sha256:4bd4a71501b6d51db4abc07e1f43f5a6fed0a1a9583cca0b401d6af50284b0db",
"sha256:57aa6198ba8acba1313c3b743e267d821a60cac77e6026caf0b55ca58d3d23be",
"sha256:5b0d657460d9f3615876fec6306e97ca15a471f6169b622d76a47e270998acf1", "sha256:5b0d657460d9f3615876fec6306e97ca15a471f6169b622d76a47e270998acf1",
"sha256:5cd36804f9f06a914a883fe682df5711d16d7b4f44d43189c5f013e7cd91e149",
"sha256:6977cf073d83358b34f93abf5c1f1193b88675fe0e4441e0e28318bc3dcba7a0", "sha256:6977cf073d83358b34f93abf5c1f1193b88675fe0e4441e0e28318bc3dcba7a0",
"sha256:718ec7a122b28d64afc5fbc3a9b99bb0545ef511373cac06fe7624520e82cb20", "sha256:718ec7a122b28d64afc5fbc3a9b99bb0545ef511373cac06fe7624520e82cb20",
"sha256:7dfbefdb3fb911ca9faed307bf309861e9995e36cca6b761c7ba6d9b77a9744a",
"sha256:801cca8923508311bf5d6d0f7da5362552e8208ebd8ec0d7b9f2cd2ff5705734", "sha256:801cca8923508311bf5d6d0f7da5362552e8208ebd8ec0d7b9f2cd2ff5705734",
"sha256:82b172e3264e62372c01b5b009b5b1a02fbb9276cbe5cc57ab00a6d6e5ed9a18",
"sha256:82d1ff571489765df2816785d532e243bde213752156c227fca595723ec5ff42",
"sha256:8580fc58074a16b749905b26cf8363f7b628dd167ba0130f5382cdc91c86b509", "sha256:8580fc58074a16b749905b26cf8363f7b628dd167ba0130f5382cdc91c86b509",
"sha256:931030d1d6282b7900e6b0a7ff9ecdb503b5e1e6781800dab2b71a9f39405bff",
"sha256:9525cd680a6f9e80c6c0af03cf973e6505c59f60b4745f682cd1a449e54b31bb", "sha256:9525cd680a6f9e80c6c0af03cf973e6505c59f60b4745f682cd1a449e54b31bb",
"sha256:a224651a81e45ef4f1d0164e256c5f6b4abb49f2ae8f22ba2f3a9d0ff338e608", "sha256:a224651a81e45ef4f1d0164e256c5f6b4abb49f2ae8f22ba2f3a9d0ff338e608",
"sha256:a370d1c570f1d72e877099651e752332444b1c5009381f043c9da5fd47f3ebae", "sha256:a370d1c570f1d72e877099651e752332444b1c5009381f043c9da5fd47f3ebae",
@ -505,12 +534,7 @@
"sha256:b85f703c2ffe539313e39ce0676bed0f355cec45a16e58c9ab7417445843047c", "sha256:b85f703c2ffe539313e39ce0676bed0f355cec45a16e58c9ab7417445843047c",
"sha256:b9f63451084a718eccdeb1e382768c94647915653af4d6019f64560d9e98642b", "sha256:b9f63451084a718eccdeb1e382768c94647915653af4d6019f64560d9e98642b",
"sha256:c793dfaa130847ccff958492b76ae8b9304e60b8a79a92962cb19e368276a22b", "sha256:c793dfaa130847ccff958492b76ae8b9304e60b8a79a92962cb19e368276a22b",
"sha256:d60c1625b108432ace8b1fa1a584017e5efa73f107d0f493c7f39c79bebf1d41", "sha256:ddd16ab250b4fc97db1c47407e78c25216a75c29d29d10ad37e51b7a2ec7b2c3"
"sha256:dc4b018d5c9b636f7546583c5591b9ea00c328c3e5871992ef5b95bac353f097",
"sha256:ddd16ab250b4fc97db1c47407e78c25216a75c29d29d10ad37e51b7a2ec7b2c3",
"sha256:e126ff4fed71e78333840c07279e1617f63cfca76d63ad5b27d65a7277206a3d",
"sha256:f8d49be8c282df8d2e1ab6ab53ab8abd859b1fa6fed384457ee85c9eff64ef97",
"sha256:fcf64c91fd44485100a2965d23bb0e227d093e91f7e776c5ca3b32574766eb56"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.0.0" "version": "==5.0.0"
@ -523,11 +547,10 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
], ],
"markers": "python_full_version >= '3.6.1'", "version": "==3.0.19"
"version": "==3.0.18"
}, },
"psycopg2": { "psycopg2": {
"hashes": [ "hashes": [
@ -562,15 +585,20 @@
"sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
"sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.7.0" "version": "==2.7.0"
}, },
"pycparser": {
"hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"version": "==2.20"
},
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
"sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
], ],
"markers": "python_version >= '3.5'",
"version": "==2.9.0" "version": "==2.9.0"
}, },
"pyparsing": { "pyparsing": {
@ -578,7 +606,6 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7" "version": "==2.4.7"
}, },
"python-dateutil": { "python-dateutil": {
@ -586,7 +613,6 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
], ],
"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.1"
}, },
"python-dotenv": { "python-dotenv": {
@ -669,11 +695,10 @@
}, },
"sendgrid": { "sendgrid": {
"hashes": [ "hashes": [
"sha256:273bdc0abec649bf6319df7b6267980f79e53ab64e92906d65eea6d4330d00b4", "sha256:1c1cca97ab968f81af43ddbbe44aade5a689da27e3e4975dc366042499620abe",
"sha256:74b0dcf9a79188948f61f456bd1bf67ffa676a5d388aba1c76bff516566d7084" "sha256:2558a8b2cf12677ceb99f8b611d914af5b9a2fd7ff3c0578e8299b4224e10071"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==6.7.1"
"version": "==6.7.0"
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
@ -685,18 +710,16 @@
}, },
"singledispatch": { "singledispatch": {
"hashes": [ "hashes": [
"sha256:58b46ce1cc4d43af0aac3ac9a047bdb0f44e05f0b2fa2eec755863331700c865", "sha256:0d428477703d8386eb6aeed6e522c9f22d49f4363cdf4ed6a2ba3dc276053e20",
"sha256:85c97f94c8957fa4e6dab113156c182fb346d56d059af78aad710bced15f16fb" "sha256:d5bb9405a4b8de48e36709238e8b91b4f6f300f81a5132ba2531a9a738eca391"
], ],
"markers": "python_version >= '2.6'", "version": "==3.6.2"
"version": "==3.6.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0" "version": "==1.16.0"
}, },
"sqlparse": { "sqlparse": {
@ -704,14 +727,13 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.4.1" "version": "==0.4.1"
}, },
"starkbank-ecdsa": { "starkbank-ecdsa": {
"hashes": [ "hashes": [
"sha256:423f81bb55c896a3c85ee98ac7da98826721eaee918f5c0c1dfff99e1972da0c" "sha256:f7b434b4a1e0ba082fb1804b908b79523973fd17b1fde377078857f7cee299d1"
], ],
"version": "==1.1.0" "version": "==1.1.1"
}, },
"text-unidecode": { "text-unidecode": {
"hashes": [ "hashes": [
@ -725,24 +747,14 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2" "version": "==0.10.2"
}, },
"traitlets": { "traitlets": {
"hashes": [ "hashes": [
"sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
"sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
], ],
"markers": "python_version >= '3.7'", "version": "==4.3.3"
"version": "==5.0.5"
},
"typing": {
"hashes": [
"sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9",
"sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==3.7.4.3"
}, },
"unidecode": { "unidecode": {
"hashes": [ "hashes": [
@ -761,11 +773,10 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
"sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.5"
"version": "==1.26.4"
}, },
"wagtail": { "wagtail": {
"hashes": [ "hashes": [
@ -821,12 +832,19 @@
} }
}, },
"develop": { "develop": {
"appnope": {
"hashes": [
"sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442",
"sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"
],
"markers": "sys_platform == 'darwin'",
"version": "==0.1.2"
},
"asgiref": { "asgiref": {
"hashes": [ "hashes": [
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.3.4" "version": "==3.3.4"
}, },
"autopep8": { "autopep8": {
@ -838,11 +856,11 @@
}, },
"awscli": { "awscli": {
"hashes": [ "hashes": [
"sha256:57ae60a3f59cac265a9e5321c618b8768fdee89565089ada271e24489be5110d", "sha256:114a945cb80677907cfa81cfa436cfe11d679303e6276bdf851294377e741045",
"sha256:a26b5e24f70cb2c542128ccc11e9d38e43cded687d60cd2ca18b3d28cd902509" "sha256:bcdcd790008547eedf46730558aa6e355e1c6c66f997d42fc7ce1329a1b1363c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.19.68" "version": "==1.19.98"
}, },
"backcall": { "backcall": {
"hashes": [ "hashes": [
@ -853,25 +871,23 @@
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:0f693f5ad6348ec1a62b3a66fee2840d3b722d66b44896022d644275ff8b143d", "sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f",
"sha256:eb3544911cb0316a33b328a27d137130af278a9c0006be0c95e5e402b01d9865" "sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.20.98"
"version": "==1.20.68"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
], ],
"version": "==2020.12.5" "version": "==2021.5.30"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"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"
}, },
"colorama": { "colorama": {
@ -879,7 +895,6 @@
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.3" "version": "==0.4.3"
}, },
"coverage": { "coverage": {
@ -942,19 +957,18 @@
}, },
"decorator": { "decorator": {
"hashes": [ "hashes": [
"sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060", "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323",
"sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98" "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"
], ],
"markers": "python_version >= '3.5'", "version": "==5.0.9"
"version": "==5.0.7"
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:db2214db1c99017cbd971e58824e6f424375154fe358afc30e976f5b99fc6060", "sha256:3339ff0e03dee13045aef6ae7b523edff75b6d726adf7a7a48f53d5a501f7db7",
"sha256:e831105edb153af1324de44d06091ca75520a227456387dda4a47d2f1cc2731a" "sha256:f2084ceecff86b1e631c2cd4107d435daf4e12f1efcdf11061a73bf0b5e95f92"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.22" "version": "==2.2.24"
}, },
"django-silk": { "django-silk": {
"hashes": [ "hashes": [
@ -969,7 +983,6 @@
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.15.2" "version": "==0.15.2"
}, },
"gprof2dot": { "gprof2dot": {
@ -983,23 +996,22 @@
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10" "version": "==2.10"
}, },
"ipdb": { "ipdb": {
"hashes": [ "hashes": [
"sha256:178c367a61c1039e44e17c56fcc4a6e7dc11b33561261382d419b6ddb4401810" "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.13.7" "version": "==0.13.9"
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:714810a5c74f512b69d5f3b944c86e592cee0a5fb9c728e582f074610f6cf038", "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64",
"sha256:f78c6a3972dde1cc9e4041cbf4de583546314ba52d3c97208e5b6b2221a9cb7d" "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf"
], ],
"index": "pypi", "index": "pypi",
"version": "==7.23.1" "version": "==7.16.1"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@ -1018,92 +1030,62 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.0.1"
"version": "==2.11.3"
}, },
"jmespath": { "jmespath": {
"hashes": [ "hashes": [
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0" "version": "==0.10.0"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5",
"sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c",
"sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be",
"sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.0.1"
"version": "==1.1.1"
},
"matplotlib-inline": {
"hashes": [
"sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811",
"sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"
],
"markers": "python_version >= '3.5'",
"version": "==0.1.2"
}, },
"parso": { "parso": {
"hashes": [ "hashes": [
"sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea",
"sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.7.1" "version": "==0.7.1"
}, },
"pexpect": { "pexpect": {
@ -1123,11 +1105,10 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
], ],
"markers": "python_full_version >= '3.6.1'", "version": "==3.0.19"
"version": "==3.0.18"
}, },
"ptyprocess": { "ptyprocess": {
"hashes": [ "hashes": [
@ -1138,19 +1119,8 @@
}, },
"pyasn1": { "pyasn1": {
"hashes": [ "hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
], ],
"version": "==0.4.8" "version": "==0.4.8"
}, },
@ -1159,7 +1129,6 @@
"sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
"sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.7.0" "version": "==2.7.0"
}, },
"pygments": { "pygments": {
@ -1167,7 +1136,6 @@
"sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
"sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
], ],
"markers": "python_version >= '3.5'",
"version": "==2.9.0" "version": "==2.9.0"
}, },
"python-dateutil": { "python-dateutil": {
@ -1175,7 +1143,6 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
], ],
"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.1"
}, },
"pytz": { "pytz": {
@ -1217,7 +1184,6 @@
"sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
"sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==5.4.1" "version": "==5.4.1"
}, },
"requests": { "requests": {
@ -1233,7 +1199,7 @@
"sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2", "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2",
"sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9" "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"
], ],
"markers": "python_version >= '3.5' and python_version < '4'", "markers": "python_version > '2.7'",
"version": "==4.7.2" "version": "==4.7.2"
}, },
"s3transfer": { "s3transfer": {
@ -1248,7 +1214,6 @@
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0" "version": "==1.16.0"
}, },
"sqlparse": { "sqlparse": {
@ -1256,7 +1221,6 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.4.1" "version": "==0.4.1"
}, },
"toml": { "toml": {
@ -1264,24 +1228,21 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
], ],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2" "version": "==0.10.2"
}, },
"traitlets": { "traitlets": {
"hashes": [ "hashes": [
"sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
"sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
], ],
"markers": "python_version >= '3.7'", "version": "==4.3.3"
"version": "==5.0.5"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
"sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.5"
"version": "==1.26.4"
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [

View File

@ -1,89 +0,0 @@
const schema = require('../../fixtures/schema_public.json');
describe('Email Verifcation', () => {
beforeEach(() => {
cy.server();
});
it('forwards to homepage if confirmation key is correct', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Registration: {
registration: {
message: 'success',
success: true
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
// user should be logged in at that stage. As the cookie cannot be set at the right time
// we just check if the user gets redirected to the login page as we can't log her in
cy.url().should('include', 'hello?redirect=%2F');
});
it('displays error if key is incorrect', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
// endpoint: '/api/graphql'
operations: {
Registration: {
registration: {
message: 'invalid_key',
success: false
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
cy.get('[data-cy="code-nok-msg"]').contains('Der angegebene Verifizierungscode ist ungültig oder abgelaufen.');
cy.get('[data-cy="code-ok-msg"]').should('not.exist');
});
it('displays error if an error occured', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
// endpoint: '/api/graphql'
operations: {
Registration: {
registration: {
message: 'unkown_error',
success: false
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
cy.get('[data-cy="code-nok-msg"]').contains('Ein Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator.');
});
it('forwards to coupon page if user has no valid license', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
// endpoint: '/api/graphql'
operations: {
Registration: {
registration: {
message: 'no_valid_license',
success: false
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
// user should be logged in at that stage. As the cookie cannot be set at the right time
// we just check if the user gets redirected to the coupon page as we can't log her in
cy.url().should('include', 'hello?redirect=%2Flicense-activation');
});
});

View File

@ -1,164 +0,0 @@
const isEmailAvailableUrl = 'https://stage.hep-verlag.ch/rest/deutsch/V1/customers/isEmailAvailable';
const registerUrl = '/api/proxy/registration/';
let registrationResponse = {
id: 84215,
group_id: 1,
confirmation: "91cf39007547feae7e33778d89fc71db",
created_at: "2020-02-06 13:56:54",
updated_at: "2020-02-06 13:56:54",
created_in: "hep verlag",
email: "feuz@aebi.ch",
firstname: "Kari",
lastname: "Feuz",
prefix: "Herr",
gender: 1,
store_id: 1,
website_id: 1,
addresses: []
};
describe('Registration', () => {
beforeEach(() => {
cy.viewport('macbook-15');
cy.server();
});
// it('works with valid data', () => {
// cy.route('POST', isEmailAvailableUrl, "true");
// cy.route('POST', registerUrl, registrationResponse);
// cy.visit('/hello');
// cy.checkEmailAvailable(registrationResponse.email);
// cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
// cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!', true);
// cy.get('[data-cy="email-check"]').contains('Eine Email ist auf dem Weg, bitte überprüfen sie ihre E-mail Konto.');
// });
it('displays error if firstname is missing', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, '', registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!', true);
cy.get('[data-cy="firstname-local-errors"]').contains('Vorname ist ein Pflichtfeld');
});
it('displays error if lastname is missing', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, '', 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!', true);
cy.get('[data-cy="lastname-local-errors"]').contains('Nachname ist ein Pflichtfeld');
});
it('displays error if street is missing', () => {
cy.route('POST', isEmailAvailableUrl, 'true');
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, '', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!', true);
cy.get('[data-cy="street-local-errors"]').contains('Strasse ist ein Pflichtfeld');
});
it('displays error if city is missing', () => {
cy.route('POST', isEmailAvailableUrl, 'true');
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', '', '3001', 'Abcd1234!', 'Abcd1234!', true);
cy.get('[data-cy="city-local-errors"]').contains('Ort ist ein Pflichtfeld');
});
it('displays error if postcode is missing', () => {
cy.route('POST', isEmailAvailableUrl, 'true');
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '', 'Abcd1234!', 'Abcd1234!', true);
cy.get('[data-cy="postcode-local-errors"]').contains('Postleitzahl ist ein Pflichtfeld');
});
it('displays error if password is missing', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', '', 'Abcd1234!', true);
cy.get('[data-cy="password-local-errors"]').contains('Passwort ist ein Pflichtfeld');
});
it('displays error if passwords are not secure', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234', 'Abcd1234', true);
cy.get('[data-cy="password-local-errors"]').contains('Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten und mindestens 8 Zeichen lang sein');
});
it('displays error if passwords are too short', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd12!', 'Abcd12!', true);
cy.get('[data-cy="password-local-errors"]').contains('Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten und mindestens 8 Zeichen lang sein');
});
it('displays error if passwords are not matching', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd129999!', true);
cy.get('[data-cy="passwordConfirmation-local-errors"]').contains('Die Bestätigung von Passwort wiederholen stimmt nicht überein');
});
it('displays error if terms are not accepted', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!', false);
cy.get('[data-cy="acceptedTerms-local-errors"]').contains('Sie müssen hier zustimmen, damit Sie sich registrieren können.');
});
it('redirects to hello if email is missing', () => {
cy.visit('/register');
cy.get('[data-cy="hello-title"]').contains('Wollen Sie mySkillbox jetzt im Unterricht verwenden?');
});
});

View File

@ -48,7 +48,7 @@ describe('The Login Page', () => {
cy.get('[data-cy=logout]').click(); cy.get('[data-cy=logout]').click();
cy.get('[data-cy=email-input]').should('exist').within(() => { cy.get('[data-cy=oauth-login]').should('exist').within(() => {
cy.visit('/beta-login'); cy.visit('/beta-login');
}); });

View File

@ -1,73 +0,0 @@
const schema = require('../../../fixtures/schema_public.json');
const isEmailAvailableUrl = '**/rest/deutsch/V1/customers/isEmailAvailable';
const checkPasswordUrl = '**/rest/deutsch/V1/integration/customer/token';
describe('Login', () => {
beforeEach(() => {
cy.server();
});
it('works with valid email and password', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Login: variables => {
return {
login: {
errors: [],
message: 'success',
success: true
}
};
},
}
});
cy.route('POST', isEmailAvailableUrl, 'false');
cy.route({
method: 'POST',
url: checkPasswordUrl,
response: 'token12345ABCD+',
});
cy.visit('/hello');
cy.checkEmailAvailable('feuz@aebi.ch');
cy.get('[data-cy="login-title"]').contains('Bitte geben Sie das passende Passwort ein');
cy.enterPassword('abcd1234');
// As we cannot set the cookie in the right manner, we just check for the absence of errors.
// In real world the user gets redirect to another page
cy.get('[data-cy="email-local-errors"]').should('not.exist');
});
it('displays error message if password is wrong', () => {
cy.viewport('macbook-15');
cy.route('POST', isEmailAvailableUrl, 'false');
cy.route({
method: 'POST',
status: 401,
response: {
message: 'Sie haben sich nicht korrekt eingeloggt oder Ihr Konto ist vor\u00fcbergehend deaktiviert.'
},
url: checkPasswordUrl
});
cy.visit('/hello');
cy.checkEmailAvailable('feuz@aebi.ch');
cy.get('[data-cy="login-title"]').contains('Bitte geben Sie das passende Passwort ein');
cy.enterPassword('abcd1234');
cy.get('[data-cy="password-errors"]').contains('Die von Ihnen eingegebene E-Mail-Adresse und das Passwort passen nicht zusammen.');
});
it('displays error message if input is not an email address', () => {
cy.viewport('macbook-15');
cy.visit('/hello');
cy.checkEmailAvailable('feuzaebi.ch');
cy.get('[data-cy="email-local-errors"]').contains('Bitte geben Sie eine gülitge E-Mail an');
});
});

View File

@ -53,7 +53,7 @@ describe('Custom Content Block', () => {
cy.viewport('macbook-15'); cy.viewport('macbook-15');
}); });
it('Deletes the custom content block and removes it from the view', () => { it.skip('Deletes the custom content block and removes it from the view', () => {
cy.fakeLogin('nico.zickgraf', 'test'); cy.fakeLogin('nico.zickgraf', 'test');
cy.visit('module/some-module'); cy.visit('module/some-module');

26894
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,6 @@
"apollo-link-http": "^1.5.16", "apollo-link-http": "^1.5.16",
"appolo": "^6.0.19", "appolo": "^6.0.19",
"autoprefixer": "^7.1.2", "autoprefixer": "^7.1.2",
"axios": "^0.18.0",
"babel-eslint": "^8.2.1", "babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
@ -87,7 +86,6 @@
"vue": "^2.5.17", "vue": "^2.5.17",
"vue-analytics": "^5.16.2", "vue-analytics": "^5.16.2",
"vue-apollo": "^3.0.0-beta.16", "vue-apollo": "^3.0.0-beta.16",
"vue-axios": "^2.1.1",
"vue-loader": "^15.9.6", "vue-loader": "^15.9.6",
"vue-matomo": "^3.13.4-0", "vue-matomo": "^3.13.4-0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",

View File

@ -20,6 +20,7 @@
import FullScreenLayout from '@/layouts/FullScreenLayout'; import FullScreenLayout from '@/layouts/FullScreenLayout';
import PublicLayout from '@/layouts/PublicLayout'; import PublicLayout from '@/layouts/PublicLayout';
import BlankLayout from '@/layouts/BlankLayout'; import BlankLayout from '@/layouts/BlankLayout';
import SplitLayout from '@/layouts/SplitLayout';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard'; import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
import EditContentBlockWizard from '@/components/content-block-form/EditContentBlockWizard'; import EditContentBlockWizard from '@/components/content-block-form/EditContentBlockWizard';
@ -50,6 +51,7 @@
FullScreenLayout, FullScreenLayout,
PublicLayout, PublicLayout,
BlankLayout, BlankLayout,
SplitLayout,
Modal, Modal,
NewContentBlockWizard, NewContentBlockWizard,
EditContentBlockWizard, EditContentBlockWizard,

View File

@ -0,0 +1,29 @@
<template>
<svg
viewBox="0 0 65.3274 22.28203"
data-name="Ebene 1"
xmlns="http://www.w3.org/2000/svg"
id="Ebene_1">
<title>Zeichenfläche 1</title>
<circle
class="cls-1"
cx="11.14101"
cy="11.14101"
r="11.14101"/>
<path
class="cls-1"
d="M38.24333,9.54537a4.55137,4.55137,0,0,0-.89344-1.506,3.95851,3.95851,0,0,0-1.37354-.97187,4.42839,4.42839,0,0,0-1.76251-.34656,4.00351,4.00351,0,0,0-1.80913.40273,3.454,3.454,0,0,0-1.30783,1.14356h-.03922V3.51107H28.425V18.74049h2.63262V11.78056a3.04623,3.04623,0,0,1,.17381-1.03228,2.49273,2.49273,0,0,1,.49813-.8468,2.33973,2.33973,0,0,1,.79169-.56277,2.52554,2.52554,0,0,1,1.03016-.20561,2.30456,2.30456,0,0,1,1.72223.68041,2.663,2.663,0,0,1,.66027,1.9236v7.00338h2.63263V11.5156A5.72892,5.72892,0,0,0,38.24333,9.54537Z"/>
<path
class="cls-1"
d="M51.53949,10.29785a5.35711,5.35711,0,0,0-1.081-1.8494,5.222,5.222,0,0,0-1.74873-1.25909,5.72952,5.72952,0,0,0-2.37085-.46844,6.03146,6.03146,0,0,0-2.2691.41333,4.92935,4.92935,0,0,0-1.7625,1.19125,5.3972,5.3972,0,0,0-1.13508,1.91618,7.81512,7.81512,0,0,0-.40062,2.59977,7.81313,7.81313,0,0,0,.40062,2.59872,5.37539,5.37539,0,0,0,1.13508,1.91617,4.91636,4.91636,0,0,0,1.7837,1.19338,6.37091,6.37091,0,0,0,2.33587.41121,6.17525,6.17525,0,0,0,1.95857-.29039,5.031,5.031,0,0,0,1.51556-.7917,4.95477,4.95477,0,0,0,1.09163-1.17,5.74653,5.74653,0,0,0,.69-1.42442l.02014-.06041H48.856l-.01378.01908a4.05035,4.05035,0,0,1-.95067.939,2.503,2.503,0,0,1-1.44243.3667,2.73106,2.73106,0,0,1-2.07516-.81289,3.29407,3.29407,0,0,1-.85634-2.07515h8.272l.00636-.03922c.01377-.089.02861-.19183.04345-.31053.01484-.1028.02967-.2215.04451-.354a4.07907,4.07907,0,0,0,.02332-.44725A6.72183,6.72183,0,0,0,51.53949,10.29785ZM46.33888,9.1331a2.61116,2.61116,0,0,1,1.94374.71539,3.20648,3.20648,0,0,1,.87224,1.7307H43.525A3.3678,3.3678,0,0,1,44.47146,9.805,2.62773,2.62773,0,0,1,46.33888,9.1331Z"/>
<path
class="cls-1"
d="M64.948,10.20883a5.456,5.456,0,0,0-1.05665-1.91512A4.53254,4.53254,0,0,0,62.273,7.12259a5.23843,5.23843,0,0,0-2.072-.40167,4.12271,4.12271,0,0,0-2.06667.50342,4.59491,4.59491,0,0,0-1.37885,1.1531H56.7046l-.32431-1.4-.00742-.036H54.19068V22.282h2.63368v-4.8674h.03816a2.65756,2.65756,0,0,0,.4589.5066,3.83173,3.83173,0,0,0,.74825.514,4.59629,4.59629,0,0,0,.9708.38048,4.445,4.445,0,0,0,1.16052.1452,5.23825,5.23825,0,0,0,2.072-.40168,4.5324,4.5324,0,0,0,1.61837-1.17111A5.43931,5.43931,0,0,0,64.948,15.473a8.40143,8.40143,0,0,0,.37942-2.63157A8.41306,8.41306,0,0,0,64.948,10.20883ZM59.759,9.1331a2.87335,2.87335,0,0,1,1.14144.22786,2.62117,2.62117,0,0,1,.93265.68359,3.25518,3.25518,0,0,1,.63166,1.15522,5.28409,5.28409,0,0,1,.22893,1.64168,5.29354,5.29354,0,0,1-.22893,1.64169,3.25917,3.25917,0,0,1-.63166,1.15415,2.59365,2.59365,0,0,1-.93265.6836,2.97289,2.97289,0,0,1-2.28288,0,2.59365,2.59365,0,0,1-.93265-.6836,3.277,3.277,0,0,1-.63166-1.15521,5.28178,5.28178,0,0,1-.22893-1.64063,5.28409,5.28409,0,0,1,.22893-1.64168,3.27711,3.27711,0,0,1,.63166-1.15522A2.62125,2.62125,0,0,1,58.6176,9.361,2.87335,2.87335,0,0,1,59.759,9.1331Z"/>
</svg>
</template>
<style scoped lang="scss">
.cls-1 {
fill: #002f6c;
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
mutation Registration($input: RegistrationInput!) {
registration(input: $input) {
success
message
}
}

View File

@ -1,18 +0,0 @@
import * as axios from 'axios';
const hepBaseUrl = process.env.HEP_URL;
export function register(registrationData) {
return axios.post('/api/proxy/registration/', registrationData);
}
export function login(username, password) {
return axios.post(`${hepBaseUrl}/rest/deutsch/V1/integration/customer/token`, {username, password});
}
export function emailExists(email) {
return axios.post(`${hepBaseUrl}/rest/deutsch/V1/customers/isEmailAvailable`, {
customerEmail: email,
websiteId: 1
});
}

View File

@ -53,6 +53,7 @@
&__content { &__content {
@include content-block(); @include content-block();
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
width: auto;
} }
&__logo { &__logo {
@ -67,7 +68,7 @@
} }
.footer { .footer {
padding-top: $large-spacing; padding: $large-spacing $medium-spacing 0;
&__content { &__content {
@include content-block(); @include content-block();

View File

@ -12,14 +12,18 @@
.simple-footer { .simple-footer {
width: 100%; width: 100%;
justify-self: center; justify-self: center;
padding: $large-spacing 0; padding: $large-spacing $small-spacing;
background-color: $color-silver-light; background-color: $color-silver-light;
display: grid; display: grid;
grid-template-columns: 1fr $footer-width 1fr; grid-template-columns: 0 1fr 0;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
height: 105px; height: 105px;
@include desktop {
grid-template-columns: 1fr $footer-width 1fr;
}
&__strong { &__strong {
@include aside-with-cheese; @include aside-with-cheese;
font-weight: 600; font-weight: 600;

View File

@ -71,7 +71,9 @@
} }
&__footer { &__footer {
grid-column: 1 / span 3; @include desktop {
grid-column: 1 / span 3;
}
} }
} }

View File

@ -0,0 +1,154 @@
<template>
<div :class="['split-view', {'split-view--illustration': illustration}]">
<div :class="['split-view__illustration', illustrationAlignment]">
<component :is="illustration"/>
</div>
<div class="split-view__content">
<router-view/>
</div>
</div>
</template>
<script>
import ContentsIllustration from '@/components/illustrations/ContentsIllustration';
import PortfolioIllustration from '@/components/illustrations/PortfolioIllustration';
import RoomsIllustration from '@/components/illustrations/RoomsIllustration';
import HelloIllustration from '@/components/illustrations/HelloIllustration';
export default {
components: {
contents: ContentsIllustration,
portfolio: PortfolioIllustration,
rooms: RoomsIllustration,
hello: HelloIllustration
},
computed: {
illustration() {
return this.$route.meta.illustration;
},
illustrationAlignment() {
return this.$route.meta.illustrationAlign ? `split-view__illustration--${this.$route.meta.illustrationAlign}` : '';
}
},
};
</script>
<style lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.split-view {
background-color: $color-brand;
display: flex;
position: relative;
width: 100%;
&--illustration {
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 50%;
background: $color-brand-dark;
}
}
&__illustration {
width: 30vw;
min-width: 400px;
align-self: center;
background-color: $color-brand;
z-index: 1;
display: none;
&--top {
align-self: start;
margin-top: $large-spacing;
}
@include desktop {
display: flex;
justify-content: center;
}
}
&__content {
background-color: $color-white;
padding: $medium-spacing;
display: flex;
flex-direction: column;
z-index: 1;
width: 100%;
@include desktop {
padding: 2*$large-spacing;
}
}
&__logo {
width: 300px;
height: 50px;
margin-bottom: $large-spacing;
@include desktop {
margin-bottom: 70px;
}
}
&__page-subheading {
@include regular-text;
color: $color-brand;
margin-bottom: $small-spacing;
}
&__page-heading {
@include heading-2;
color: $color-brand;
margin-bottom: 2*$large-spacing;
}
&__heading {
@include heading-2;
margin-bottom: $small-spacing;
}
&__claim {
@include heading-2;
margin-bottom: 70px;
}
&__paragraph {
@include regular-text;
margin-bottom: $medium-spacing;
&:last-of-type {
margin-bottom: 2*$large-spacing;
}
}
&__button {
@include regular-text;
flex-grow: 0;
align-self: flex-start;
min-width: 150px;
display: inline-flex;
box-sizing: border-box;
justify-content: center;
margin-bottom: $large-spacing;
cursor: pointer;
}
&__secondary-link {
@include inline-title;
cursor: pointer;
@include desktop {
margin-top: auto;
}
}
}
</style>

View File

@ -1,12 +1,10 @@
import '@babel/polyfill'; import '@babel/polyfill';
import Vue from 'vue'; import Vue from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import VueVimeoPlayer from 'vue-vimeo-player'; import VueVimeoPlayer from 'vue-vimeo-player';
import apolloClientFactory from './graphql/client'; import apolloClientFactory from './graphql/client';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import App from './App'; import App from './App';
import router from './router'; import {router, postLoginRedirectUrlKey} from './router';
import store from '@/store/index'; import store from '@/store/index';
import VueScrollTo from 'vue-scrollto'; import VueScrollTo from 'vue-scrollto';
import {Validator, install as VeeValidate} from 'vee-validate/dist/vee-validate.minimal.esm.js'; import {Validator, install as VeeValidate} from 'vee-validate/dist/vee-validate.minimal.esm.js';
@ -28,7 +26,6 @@ const isProduction = process.env.NODE_ENV === 'production';
Vue.use(VueModal); Vue.use(VueModal);
Vue.use(VueRemoveEdges); Vue.use(VueRemoveEdges);
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(VueAxios, axios);
Vue.use(VueVimeoPlayer); Vue.use(VueVimeoPlayer);
Vue.use(VueToast); Vue.use(VueToast);
Vue.use(VueLogger, { Vue.use(VueLogger, {
@ -161,7 +158,13 @@ router.beforeEach(async (to, from, next) => {
} }
if (unauthorizedAccess(to)) { if (unauthorizedAccess(to)) {
const redirectUrl = `/hello?redirect=${to.path}`; const postLoginRedirectionUrl = to.path;
const redirectUrl = `/hello/`;
if (window.localStorage) {
localStorage.setItem(postLoginRedirectUrlKey, postLoginRedirectionUrl);
}
next(redirectUrl); next(redirectUrl);
return; return;
} }

View File

@ -72,13 +72,6 @@
class="button button--primary button--big actions__submit" class="button button--primary button--big actions__submit"
data-cy="login-button">Anmelden</button> data-cy="login-button">Anmelden</button>
</div> </div>
<div class="account-link">
<p class="account-link__text">Haben Sie noch kein Konto?</p>
<router-link
:to="{name: 'registration'}"
class="account-link__link text-link">Jetzt registrieren
</router-link>
</div>
</form> </form>
</div> </div>
</template> </template>

View File

@ -1,19 +0,0 @@
<template>
<div class="check-email">
<main
class="check-email__content content"
data-cy="email-check">
<p class="content__instructions">Eine E-Mail ist auf dem Weg, bitte überprüfen Sie Ihr Postfach.</p>
</main>
</div>
</template>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.content__instructions {
margin-top: $medium-spacing;
}
</style>

View File

@ -1,96 +0,0 @@
<template>
<div class="emailconfirmation public-page">
<h1 class="emailconfirmation__title public-page__title">Überprüfung der E-Mail Adresse</h1>
<p v-if="loading">Der Verifikationscode wird überprüft.</p>
<p
data-cy="code-ok-msg"
v-if="showOkMessage">Der Verifikationscode ist gültig. Sie werden weitergeleitet.</p>
<p
data-cy="code-nok-msg"
v-if="showErrorMessage">{{ errorMessage }}</p>
</div>
</template>
<script>
import REGISTRATION_MUTATION from '@/graphql/gql/mutations/registration.gql';
export default {
data() {
return {
loading: true,
keyValid: false,
errorMessage: ''
};
},
computed: {
showOkMessage() {
return !this.loading && this.keyValid;
},
showErrorMessage() {
return !this.loading && !this.keyValid;
}
},
mounted() {
this.$apollo.mutate({
mutation: REGISTRATION_MUTATION,
client: 'publicClient',
variables: {
input: {
confirmationKey: this.$route.query.confirmation,
userId: this.$route.query.id
}
},
fetchPolicy: 'no-cache'
}).then(({data}) => {
this.loading = false;
if (data.registration.success) {
this.keyValid = true;
if (data.registration.message === 'no_valid_license') {
this.$router.push({name: 'licenseActivation'});
} else {
this.$router.push('/');
}
} else {
switch (data.registration.message) {
case 'invalid_key':
this.errorMessage = 'Der angegebene Verifizierungscode ist ungültig oder abgelaufen.';
break;
case 'no_valid_license':
this.$router.push({name: 'licenseActivation'});
break;
default:
this.errorMessage = 'Ein Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator.';
}
}
})
.catch(() => {
this.errorMessage = 'Ein Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator.';
});
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
</style>

View File

@ -1,39 +0,0 @@
<template>
<div class="forgot-password public-page">
<header class="info-header">
<h1
class="forgot-password__title public-page__title"
data-cy="forgot-password">Passwort vergessen?</h1>
</header>
<section class="forgot-password__section forgot-password__text">
<p class="forgot-info">
Ihr Benutzerkonto wird durch den hep Verlag verwaltet. Um Ihr Passwort zurückzusetzen,
gehen Sie
auf die Verlagsseite.</p>
</section>
<section class="forgot-password__section forgot-password__text">
<p class="forgot-info">
Passwort unter
<a
class="hep-link"
href="https://www.hep-verlag.ch/customer/account/forgotpassword/"
target="_blank">www.hep-verlag.ch</a> zurücksetzen</p>
</section>
</div>
</template>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.forgot-info {
font-family: $sans-serif-font-family;
margin-bottom: $medium-spacing;
}
.forgot-password__link {
margin-top: $large-spacing;
}
</style>

View File

@ -1,59 +1,81 @@
<template> <template>
<div class="hello public-page"> <div
<h1 class="hello"
class="hello__title public-page__title" data-cy="hello-page">
data-cy="hello-title">Wollen Sie {{ pageTitle }} jetzt im Unterricht verwenden?</h1> <div class="about">
<form <div class="about__logos logos">
class="hello__form hello-form" <a
novalidate href="https://www.hep-verlag.ch/"
@submit.prevent="validateBeforeSubmit"> target="_blank">
<div class="hello-form__field skillboxform-input"> <hep-logo-no-claim class="logos__logo" />
<label </a>
for="email" <a
class="skillboxform-input__label">E-Mail</label> href="https://www.ehb.swiss/"
<input target="_blank">
v-model="email" <ehb-logo class="logos__logo" />
v-validate="'required'" </a>
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
name="email"
type="email"
data-vv-as="E-Mail"
class="change-form__email skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="email-input"
placeholder="E-Mail eingeben"
tabindex="0"
id="email"
>
<small
class="skillboxform-input__error"
data-cy="email-local-errors"
v-if="errors.has('email') && submitted"
>{{ errors.first('email') }}</small>
</div> </div>
<div class="actions"> <p class="about__text">mySkillbox ist ein Angebot des hep Verlags in
<loading-button Zusammenarbeit mit dem Eidgenössischen Hochschulinstitut für Berufsbildung.</p>
:loading="loading" </div>
label="Los geht's" <logo class="logo" />
class="actions__submit" <div class="login-actions">
data-cy="hello-button" <h2
/> class="login-actions__title"
data-cy="hello-title">Wollen Sie {{ pageTitle }} im Unterricht verwenden?</h2>
<a
class="button button--primary button--big actions__submit"
href="/api/oauth/login/"
data-cy="oauth-login">Mit hep Konto anmelden</a>
<div class="login-actions__register register">
<p>Haben Sie noch kein hep Konto?</p>
<a
class="hep-link"
href="/api/oauth/login/"
data-cy="oauth-login">Jetzt registrieren</a>
</div> </div>
</form> </div>
<div class="information">
<p>Was ist ein hep Konto und wie kann ich mich dafür registrieren?</p>
<a
class="hep-link"
href="https://myskillbox.ch/anleitung"
data-cy="oauth-login">Anleitung anschauen</a>
</div>
<div class="links">
<ul class="links__list">
<li class="links__list-item">
<a href="">Lizenz kaufen</a>
</li>
<li class="links__list-item">
<a href="">Support</a>
</li>
<li class="links__list-item">
<a href="">Datenschutz</a>
</li>
</ul>
</div>
</div> </div>
</template> </template>
<script> <script>
import {emailExists} from '../hep-client/index';
import HELLO_EMAIL_MUTATION from '@/graphql/gql/local/mutations/helloEmail.gql';
import LoadingButton from '@/components/LoadingButton'; import LoadingButton from '@/components/LoadingButton';
import pageTitleMixin from '@/mixins/page-title'; import pageTitleMixin from '@/mixins/page-title';
import HepLogoNoClaim from '@/components/icons/HepLogoNoClaim';
import EhbLogo from '@/components/icons/EhbLogo';
import Logo from '@/components/icons/Logo';
import HelloIllustration from '@/components/illustrations/HelloIllustration';
export default { export default {
mixins: [pageTitleMixin], mixins: [pageTitleMixin],
components: { components: {
LoadingButton LoadingButton,
HelloIllustration,
HepLogoNoClaim,
EhbLogo,
Logo
}, },
data() { data() {
@ -64,41 +86,6 @@
}; };
}, },
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
if (result) {
this.loading = true;
emailExists(this.email).then((response) => {
let redirectRouteName = 'login';
if (response.data) {
redirectRouteName = 'registration';
}
this.$apollo.mutate({
mutation: HELLO_EMAIL_MUTATION,
variables: {
helloEmail: this.email
}
}).then(() => {
this.$router.push({name: redirectRouteName, query: this.$route.query});
this.loading = false;
});
})
.catch(() => {
this.loading = false;
this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals.';
});
}
});
},
resetForm() {
this.email = '';
this.submitted = false;
this.$validator.reset();
}
}
}; };
</script> </script>
@ -106,15 +93,106 @@
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss"; @import "@/styles/_mixins.scss";
.text-link { $hello-block-margin: 2*$medium-spacing;
font-family: $sans-serif-font-family;
color: $color-brand; .hello {
max-width: 600px;
margin: 0 auto;
@include desktop {
max-width: 600px;
margin: 0 auto;
}
} }
.actions { .logo {
&__reset { display: block;
display: inline-block; width: 300px;
margin-left: $large-spacing; margin: $small-spacing auto $hello-block-margin;
@include desktop {
display: none;
}
}
.about {
display: none;
margin-bottom: $hello-block-margin;
@include desktop {
display: block;
}
&__text {
margin-top: $medium-spacing;
@include regular-text;
}
&__logos {
& a:first-child {
margin-right: $large-spacing;
}
}
}
.logos {
&__logo {
height: 30px;
}
}
.login-actions {
@include widget-shadow;
padding: $medium-spacing;
margin-bottom: $hello-block-margin;
&__title {
font-size: 2.125rem; // 34px
font-weight: 700;
}
&__register {
margin-top: $large-spacing;
> p, a {
@include regular-text;
}
}
}
.information {
margin-top: $hello-block-margin;
> p, a {
@include regular-text;
}
}
.links {
margin-top: $hello-block-margin;
display: flex;
&__list-item {
color: $color-silver-dark;
> a {
@include regular-text;
}
flex-direction: column;
margin-top: $medium-spacing;
&:first-child {
margin-top: 0;
}
@include desktop {
display: inline-block;
flex-direction: row;
&:not(:last-child) {
margin-right: 1rem;
}
}
} }
} }

View File

@ -0,0 +1,43 @@
<template>
<div class="login-error">
<header class="info-header">
<h1
class="public-page__title"
data-cy="login-error-title">{{ title }}</h1>
</header>
<main
class="login-error__content content"
data-cy="login-error-text">
<p class="content__instructions">
{{ errorMessage }}
</p>
<p>
<router-link
:to="{name: 'hello'}"
class="hep-link">
Startseite anzeigen
</router-link>
</p>
</main>
</div>
</template>
<script>
export default {
name: 'LoginErrorPage',
props: ['title', 'errorMessage']
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.content__instructions {
margin: $medium-spacing 0;
@include regular-text;
}
</style>

View File

@ -1,197 +0,0 @@
<template>
<div class="login public-page">
<header class="info-header">
<p class="info-header__text small-emph">Super, wir haben für <span
class="info-header__emph">{{ helloEmail.email }}</span> ein hep-Konto gefunden.</p>
<h1
class="login__title public-page__title"
data-cy="login-title">Bitte geben Sie das passende Passwort ein</h1>
</header>
<form
class="login__form login-form"
novalidate
@submit.prevent="validateBeforeSubmit">
<div class="change-form__field skillboxform-input">
<label
for="password"
class="skillboxform-input__label">Passwort</label>
<input
v-model="password"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('password') }"
name="password"
type="password"
data-vv-as="Passwort"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="password-input"
tabindex="0"
id="password"
>
<small
:key="error"
class="skillboxform-input__error"
data-cy="password-errors"
v-for="error in passwordErrors"
>{{ error }}</small>
</div>
<div class="actions">
<loading-button
:loading="loading"
class="actions__submit"
data-cy="login-button"
label="Anmelden"
/>
<router-link
:to="{name: 'hello'}"
tag="button"
class="button button--big actions__submit back-button">Abbrechen
</router-link>
<router-link
:to="{name: 'forgotPassword'}"
class="actions__reset text-link">Passwort vergessen?</router-link>
</div>
</form>
</div>
</template>
<script>
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
import LOGIN_MUTATION from '@/graphql/gql/mutations/login.gql';
import {login} from '@/hep-client/index';
import LoadingButton from '@/components/LoadingButton';
export default {
components: {LoadingButton},
data() {
return {
password: '',
passwordErrors: [],
submitted: false,
loading: false
};
},
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
if (result) {
this.loading = true;
login(this.helloEmail.email, this.password)
.then((response) => {
this.loading = false;
if (response.status === 200) {
this.mySkillboxLogin(response.data);
} else {
this.passwordErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'];
}
})
.catch((error) => {
this.loading = false;
if (error.response.data.message && error.response.data.message === 'Sie haben sich nicht korrekt eingeloggt oder Ihr Konto ist vor\u00fcbergehend deaktiviert.') {
this.passwordErrors = ['Die von Ihnen eingegebene E-Mail-Adresse und das Passwort passen nicht zusammen.'];
} else {
this.passwordErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'];
}
});
}
});
},
mySkillboxLogin(token) {
const that = this;
this.$apollo.mutate({
client: 'publicClient',
mutation: LOGIN_MUTATION,
variables: {
input: {
tokenInput: token
}
},
update(
store,
{
data: {
login
}
}
) {
try {
if (login.success) {
if (login.message === 'no_valid_license') {
that.$router.push({name: 'licenseActivation'});
} else {
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/';
that.$router.push(redirectUrl);
}
}
} catch (e) {
that.passwordErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'];
}
}
})
.catch(errors => {
const firstError = errors.graphQLErrors[0];
switch (firstError.message) {
case 'invalid_credentials':
that.passwordErrors = ['Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.'];
break;
case 'email_not_verified':
that.passwordErrors = ['Bitte verifiziere zuerst deine E-Mail.'];
break;
case 'no_valid_license':
this.$router.push({name: 'licenseActivation'});
break;
}
});
},
resetForm() {
this.password = '';
this.submitted = false;
this.$validator.reset();
},
},
apollo: {
helloEmail: {
query: HELLO_EMAIL,
result({data: {helloEmail}}) {
if (helloEmail.email === '') {
this.$router.push({name: 'hello'});
}
}
},
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
display: flex;
&__reset {
display: inline-block;
margin-left: auto;
line-height: 19px;;
padding: $small-spacing;
}
}
.back-button {
//font-size: 1rem;
line-height: normal;
margin-left: $medium-spacing;
}
</style>

View File

@ -1,40 +1,26 @@
<template> <template>
<div <div
:class="['onboarding', {'onboarding--illustration': illustration}]" class="onboarding__content"
data-cy="onboarding-page"> data-cy="onboarding-page">
<div class="onboarding__illustration"> <router-view/>
<component :is="illustration"/> <a
</div> class="onboarding__button button button--primary button--big"
<div class="onboarding__content"> data-cy="onboarding-next-link"
<router-view/> @click="next">{{ nextLabel }}
<a </a>
class="onboarding__button button button--primary button--big" <a
data-cy="onboarding-next-link" class="onboarding__secondary-link"
@click="next">{{ nextLabel }} data-cy="onboarding-skip-link"
</a> @click.prevent="completeOnboarding">Einführung überspringen</a>
<a
class="onboarding__secondary-link"
data-cy="onboarding-skip-link"
@click.prevent="completeOnboarding">Einführung überspringen</a>
</div>
</div> </div>
</template> </template>
<script> <script>
import ContentsIllustration from '@/components/illustrations/ContentsIllustration';
import PortfolioIllustration from '@/components/illustrations/PortfolioIllustration';
import RoomsIllustration from '@/components/illustrations/RoomsIllustration';
import UPDATE_ONBOARDING_PROGRESS from '@/graphql/gql/mutations/updateOnboardingProgress.gql'; import UPDATE_ONBOARDING_PROGRESS from '@/graphql/gql/mutations/updateOnboardingProgress.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery'; import ME_QUERY from '@/graphql/gql/queries/meQuery';
export default { export default {
components: {
contents: ContentsIllustration,
portfolio: PortfolioIllustration,
rooms: RoomsIllustration
},
computed: { computed: {
nextLabel() { nextLabel() {
return this.$route.name === 'onboarding-start' ? 'Einführung starten' : 'Weiter'; return this.$route.name === 'onboarding-start' ? 'Einführung starten' : 'Weiter';

View File

@ -1,423 +0,0 @@
<template>
<div class="registration public-page">
<header class="info-header">
<p class="info-header__text small-emph">Für <span class="info-header__emph">{{ helloEmail }}</span> haben wir kein
Hep Konto gefunden.</p>
<h1
class="registration__title public-page__title"
data-cy="registration-title">Damit Sie {{ pageTitle }} verwenden können, müssen Sie ein Konto erstellen.</h1>
</header>
<form
class="registration__form registration-form"
novalidate
@submit.prevent="validateBeforeSubmit">
<div class="registration-form__field skillboxform-input">
<div class="registration-form__field skillboxform-input">
<label
for="prefix"
class="skillboxform-input__label">Anrede</label>
<select
v-model="prefix"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('prefix') }"
name="prefix"
data-vv-as="Prefix"
class="change-form__prefix skillbox-input skillboxform-input__input skillbox-dropdown"
autocomplete="off"
data-cy="prefix-selection"
id="prefix"
>
<option
value="Herr"
selected>Herr
</option>
<option value="Frau">Frau</option>
</select>
</div>
<label
for="firstname"
class="skillboxform-input__label">Vorname</label>
<input
v-model="firstname"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('firstname') }"
name="firstname"
type="text"
data-vv-as="Vorname"
class="change-form__firstname skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="firstname-input"
id="firstname"
>
<small
class="skillboxform-input__error"
data-cy="firstname-local-errors"
v-if="errors.has('firstname') && submitted"
>{{ errors.first('firstname') }}</small>
<small
:key="error"
class="skillboxform-input__error"
data-cy="firstname-remote-errors"
v-for="error in firstnameErrors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label
for="lastname"
class="skillboxform-input__label">Nachname</label>
<input
v-model="lastname"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('lastname') }"
name="lastname"
type="text"
data-vv-as="Nachname"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="lastname-input"
id="lastname"
>
<small
class="skillboxform-input__error"
data-cy="lastname-local-errors"
v-if="errors.has('lastname') && submitted"
>{{ errors.first('lastname') }}</small>
<small
:key="error"
class="skillboxform-input__error"
data-cy="lastname-remote-errors"
v-for="error in lastnameErrors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label
for="street"
class="skillboxform-input__label">Strasse</label>
<input
v-model="street"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('street') }"
name="street"
type="text"
data-vv-as="Strasse"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="street-input"
id="street"
>
<small
class="skillboxform-input__error"
data-cy="street-local-errors"
v-if="errors.has('street') && submitted"
>{{ errors.first('street') }}</small>
<small
:key="error"
class="skillboxform-input__error"
data-cy="street-remote-errors"
v-for="error in streetErrors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label
for="city"
class="skillboxform-input__label">Ort</label>
<input
v-model="city"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('city') }"
name="city"
type="text"
data-vv-as="Ort"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="city-input"
id="city"
>
<small
class="skillboxform-input__error"
data-cy="city-local-errors"
v-if="errors.has('city') && submitted"
>{{ errors.first('city') }}</small>
<small
:key="error"
class="skillboxform-input__error"
data-cy="city-remote-errors"
v-for="error in cityErrors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label
for="postcode"
class="skillboxform-input__label">Postleitzahl</label>
<input
v-model="postcode"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('postcode') }"
name="postcode"
type="text"
data-vv-as="Postleitzahl"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="postcode-input"
id="postcode"
>
<small
class="skillboxform-input__error"
data-cy="postcode-local-errors"
v-if="errors.has('postcode') && submitted"
>{{ errors.first('postcode') }}</small>
<small
:key="error"
class="skillboxform-input__error"
data-cy="postcode-remote-errors"
v-for="error in postcodeErrors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label
for="password"
class="skillboxform-input__label">Passwort</label>
<input
v-model="password"
v-validate="'required|strongPassword'"
:class="{ 'skillboxform-input__input--error': errors.has('password') && submitted }"
name="password"
type="text"
data-vv-as="Passwort"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="password-input"
id="password"
ref="password"
>
<small
class="skillboxform-input__error"
data-cy="password-local-errors"
v-if="errors.has('password') && submitted"
>{{ errors.first('password') }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label
for="password2"
class="skillboxform-input__label">Passwort wiederholen</label>
<input
v-model="passwordConfirmation"
v-validate="'required|confirmed:password'"
:class="{ 'skillboxform-input__input--error': errors.has('password-confirmation') && submitted }"
name="password-confirmation"
type="text"
data-vv-as="Passwort wiederholen"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="passwordConfirmation-input"
id="password-confirmation"
>
<small
class="skillboxform-input__error"
data-cy="passwordConfirmation-local-errors"
v-if="errors.has('password-confirmation') && submitted"
>{{ errors.first('password-confirmation') }}</small>
</div>
<div class="change-form__field skillboxform-input">
<checkbox
v-model="acceptedTerms"
:checked="acceptedTerms"
v-validate="'required:true'"
:class="{ 'skillboxform-input__input--error': errors.has('accepted-terms') && submitted}"
name="accepted-terms"
class="skillboxform-input__checkbox"
data-cy="acceptedTerms-input"
id="accepted-terms"
>
<span>Hiermit akzeptiere ich die <a
href="https://www.hep-verlag.ch/agb"
target="_blank"
class="hep-link">AGB</a> und
<a
href="https://www.hep-verlag.ch/datenschutz"
target="_blank"
class="hep-link">Datenschutzbestimmungen</a></span>
</checkbox>
<small
class="skillboxform-input__error"
data-cy="acceptedTerms-local-errors"
v-if="errors.has('accepted-terms') && submitted"
>Sie müssen hier zustimmen, damit Sie sich registrieren können.</small>
</div>
<div class="skillboxform-input">
<small
class="skillboxform-input__error"
data-cy="registration-error"
v-if="registrationError">{{ registrationError }}</small>
</div>
<div class="actions">
<loading-button
:loading="loading"
class="actions__submit"
label="Konto erstellen"
data-cy="register-button"
/>
</div>
</form>
</div>
</template>
<script>
import {register} from '../hep-client/index';
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
import Checkbox from '@/components/ui/Checkbox';
import LoadingButton from '@/components/LoadingButton';
import pageTitleMixin from '@/mixins/page-title';
function initialData() {
return {
prefix: 'Herr',
lastname: '',
firstname: '',
password: '',
passwordConfirmation: '',
street: '',
postcode: '',
city: '',
acceptedTerms: false,
firstnameErrors: '',
lastnameErrors: '',
emailErrors: '',
passwordsErrors: [],
passwordErrors: [],
streetErrors: [],
cityErrors: [],
postcodeErrors: [],
registrationError: '',
acceptedTermsError: [],
submitted: false,
};
}
export default {
mixins: [pageTitleMixin],
components: {
LoadingButton,
Checkbox
},
data() {
return Object.assign(
{
helloEmail: '',
loading: false,
},
initialData()
);
},
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
if (result) {
this.loading = true;
const registrationData = {
customer: {
prefix: this.prefix,
email: this.helloEmail,
firstname: this.firstname,
lastname: this.lastname,
gender: this.prefix === 'Herr' ? 1 : 2,
addresses: [{
street: [this.street],
postcode: this.postcode,
city: this.city,
country_id: 'CH',
firstname: this.firstname,
lastname: this.lastname,
prefix: this.prefix,
default_shipping: true,
default_billing: true
}]
},
password: this.password,
accepted_terms: this.acceptedTerms
};
register(registrationData).then((response) => {
this.loading = false;
if (response.data.id && response.data.id > 0) {
this.$router.push({name: 'checkEmail'});
}
})
.catch((error) => {
this.loading = false;
console.warn(error);
if (error.response.data.message) {
switch (error.response.data.message) {
case 'Ein Kunde mit der gleichen E-Mail-Adresse existiert bereits in einer zugeordneten Website.':
this.emailErrors = ['Die angegebene E-Mail ist bereits registriert.'];
break;
case 'Sie müssen hier zustimmen, damit Sie sich registrieren können.':
this.acceptedTermsError = ['Sie müssen hier zustimmen, damit Sie sich registrieren können.'];
break;
default:
this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
}
} else {
this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
}
});
}
});
},
resetForm() {
Object.assign(this.$data, initialData());
this.$validator.reset();
}
},
apollo: {
helloEmail: {
query: HELLO_EMAIL,
result({data}) {
if (data.helloEmail && data.helloEmail.email === '') {
this.$router.push({name: 'hello'});
}
},
update(data) {
return data.helloEmail.email;
},
error() {
console.log('error');
this.$router.push({name: 'hello'});
}
},
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
.registration {
&__text {
font-family: $sans-serif-font-family;
margin-bottom: $small-spacing;
}
}
</style>

View File

@ -1,25 +1,17 @@
import login from '@/pages/login';
import hello from '@/pages/hello'; import hello from '@/pages/hello';
import betaLogin from '@/pages/beta-login'; import betaLogin from '@/pages/beta-login';
import registration from '@/pages/registration'; import loginError from '@/pages/login-error';
export default [ export default [
{
path: '/login',
name: 'login',
component: login,
meta: {
layout: 'public',
public: true,
},
},
{ {
path: '/hello', path: '/hello',
name: 'hello', name: 'hello',
component: hello, component: hello,
meta: { meta: {
layout: 'public', layout: 'split',
public: true, public: true,
illustration: 'hello',
illustrationAlign: 'top'
}, },
}, },
{ {
@ -32,12 +24,29 @@ export default [
}, },
}, },
{ {
path: '/register', path: '/verify-email',
component: registration, component: loginError,
name: 'registration', name: 'verifyEmail',
props: {
title: 'Bitte schauen Sie in Ihr Postfach',
errorMessage: 'Ihre E-Mail-Adresse ist noch nicht verifiziert. Wir haben eine E-Mail mit einem Aktivierungslink an Sie verschickt.'
},
meta: { meta: {
public: true, public: true,
layout: 'public', layout: 'public',
}, },
} },
{
path: '/unknown-auth-error',
component: loginError,
name: 'unknownAuthError',
props: {
title: 'Es ist ein Fehler aufgetreten',
errorMessage: 'Es tut uns leid, dass mySkillbox im Moment nicht wie erwartet funktioniert. Bitte versuchen Sie es später nochmals.'
},
meta: {
public: true,
layout: 'public',
},
},
]; ];

View File

@ -9,10 +9,7 @@ import submission from '@/pages/studentSubmission';
import Router from 'vue-router'; import Router from 'vue-router';
import surveyPage from '@/pages/survey'; import surveyPage from '@/pages/survey';
import styleGuidePage from '@/pages/styleguide'; import styleGuidePage from '@/pages/styleguide';
import checkEmail from '@/pages/check-email';
import emailVerification from '@/pages/email-verification';
import licenseActivation from '@/pages/license-activation'; import licenseActivation from '@/pages/license-activation';
import forgotPassword from '@/pages/forgot-password';
import joinClass from '@/pages/joinClass'; import joinClass from '@/pages/joinClass';
import news from '@/pages/news'; import news from '@/pages/news';
@ -25,12 +22,15 @@ import roomRoutes from './room.routes';
import store from '@/store/index'; import store from '@/store/index';
import {LAYOUT_SIMPLE} from '@/router/core.constants'; import {LAYOUT_SIMPLE} from '@/router/core.constants';
import {EMAIL_NOT_VERIFIED_STATE, NO_VALID_LICENSE_STATE, SUCCESS_STATE} from './oauth.names';
const postLoginRedirectUrlKey = 'postLoginRedirectionUrl';
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: start, component: start
}, },
...moduleRoutes, ...moduleRoutes,
...authRoutes, ...authRoutes,
@ -55,24 +55,6 @@ const routes = [
props: true, props: true,
meta: {layout: LAYOUT_SIMPLE}, meta: {layout: LAYOUT_SIMPLE},
}, },
{
path: '/check-email',
component: checkEmail,
name: 'checkEmail',
meta: {
public: true,
layout: 'public',
},
},
{
path: '/verify-email',
component: emailVerification,
name: 'emailVerification',
meta: {
public: true,
layout: 'public',
},
},
{ {
path: '/license-activation', path: '/license-activation',
component: licenseActivation, component: licenseActivation,
@ -81,20 +63,31 @@ const routes = [
layout: 'public', layout: 'public',
}, },
}, },
{
path: '/forgot-password',
component: forgotPassword,
name: 'forgotPassword',
meta: {
layout: 'public',
public: true,
},
},
{ {
path: '/news', path: '/news',
component: news, component: news,
name: 'news', name: 'news',
}, },
{
path: '/oauth-redirect',
redirect: to => {
switch (to.query.state) {
case EMAIL_NOT_VERIFIED_STATE:
return '/verify-email';
case NO_VALID_LICENSE_STATE:
return '/license-activation';
case SUCCESS_STATE:
if (window.localStorage && localStorage.getItem(postLoginRedirectUrlKey)) {
const redirectUrl = localStorage.getItem(postLoginRedirectUrlKey);
localStorage.removeItem(postLoginRedirectUrlKey);
return redirectUrl;
}
return '/';
default:
return '/unknown-auth-error';
}
}
},
{path: '/styleguide', component: styleGuidePage}, {path: '/styleguide', component: styleGuidePage},
{ {
path: '*', path: '*',
@ -122,4 +115,5 @@ router.afterEach((to, from) => {
store.commit('setEditModule', false); store.commit('setEditModule', false);
store.dispatch('showMobileNavigation', false); store.dispatch('showMobileNavigation', false);
}); });
export default router;
export {router, postLoginRedirectUrlKey};

View File

@ -0,0 +1,3 @@
export const EMAIL_NOT_VERIFIED_STATE = 'email_not_verified';
export const NO_VALID_LICENSE_STATE = 'no_valid_license';
export const SUCCESS_STATE = 'success';

View File

@ -16,7 +16,7 @@ export default [
component: onboardingStart, component: onboardingStart,
name: 'onboarding-start', name: 'onboarding-start',
meta: { meta: {
layout: 'blank', layout: 'split',
next: ONBOARDING_STEP_1, next: ONBOARDING_STEP_1,
}, },
}, },
@ -25,7 +25,7 @@ export default [
component: onboardingStep1, component: onboardingStep1,
name: ONBOARDING_STEP_1, name: ONBOARDING_STEP_1,
meta: { meta: {
layout: 'blank', layout: 'split',
next: ONBOARDING_STEP_2, next: ONBOARDING_STEP_2,
illustration: 'contents', illustration: 'contents',
}, },
@ -35,7 +35,7 @@ export default [
component: onboardingStep2, component: onboardingStep2,
name: ONBOARDING_STEP_2, name: ONBOARDING_STEP_2,
meta: { meta: {
layout: 'blank', layout: 'split',
next: ONBOARDING_STEP_3, next: ONBOARDING_STEP_3,
illustration: 'rooms', illustration: 'rooms',
}, },
@ -45,7 +45,7 @@ export default [
component: onboardingStep3, component: onboardingStep3,
name: ONBOARDING_STEP_3, name: ONBOARDING_STEP_3,
meta: { meta: {
layout: 'blank', layout: 'split',
next: 'home', next: 'home',
illustration: 'portfolio', illustration: 'portfolio',
}, },

File diff suppressed because it is too large Load Diff

73
package-lock.json generated
View File

@ -1,8 +1,60 @@
{ {
"name": "cariot", "name": "cariot",
"version": "1.0.1", "version": "1.0.1",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "cariot",
"version": "1.0.1",
"dependencies": {
"babel-polyfill": "^6.26.0",
"unfetch": "^3.0.0"
},
"engines": {
"node": "8.x"
}
},
"node_modules/babel-polyfill": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
"integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
"dependencies": {
"babel-runtime": "^6.26.0",
"core-js": "^2.5.0",
"regenerator-runtime": "^0.10.5"
}
},
"node_modules/babel-runtime": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dependencies": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
}
},
"node_modules/babel-runtime/node_modules/regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
"node_modules/core-js": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
},
"node_modules/regenerator-runtime": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
},
"node_modules/unfetch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.1.tgz",
"integrity": "sha512-syDl3htvM56w0HC0PTVA5jEEknOCJ3dWgWGDuaEtQUno8ORDCfZQbm12RzfWO3AC3YhWDoP61dlgmo8Z05Y97g=="
}
},
"dependencies": { "dependencies": {
"babel-polyfill": { "babel-polyfill": {
"version": "6.26.0", "version": "6.26.0",
@ -35,11 +87,6 @@
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
}, },
"es6-object-assign": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
},
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.10.5", "version": "0.10.5",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
@ -49,20 +96,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.1.tgz", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.1.tgz",
"integrity": "sha512-syDl3htvM56w0HC0PTVA5jEEknOCJ3dWgWGDuaEtQUno8ORDCfZQbm12RzfWO3AC3YhWDoP61dlgmo8Z05Y97g==" "integrity": "sha512-syDl3htvM56w0HC0PTVA5jEEknOCJ3dWgWGDuaEtQUno8ORDCfZQbm12RzfWO3AC3YhWDoP61dlgmo8Z05Y97g=="
},
"vue": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
},
"vuejs-logger": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/vuejs-logger/-/vuejs-logger-1.5.5.tgz",
"integrity": "sha512-wESz1F4KWk98YANEDg2yeS+fpwk2WrR41ZslLfZgTD+EYFm/7VMMUjRThhHT8CCOLOCQdsS4Ge2C9bIs68v8Ww==",
"requires": {
"es6-object-assign": "1.1.0",
"vue": "2.6.11"
}
} }
} }
} }

View File

@ -8,8 +8,6 @@ export EMAIL_HOST_PASSWORD=
export EMAIL_HOST_USER= export EMAIL_HOST_USER=
export EMAIL_PORT= export EMAIL_PORT=
export GOOGLE_ANALYTICS_ID= export GOOGLE_ANALYTICS_ID=
export HEP_ADMIN_PASSWORD=
export HEP_ADMIN_USER=
export HEP_URL= export HEP_URL=
export HEP_URL=https://stage.hep-verlag.ch export HEP_URL=https://stage.hep-verlag.ch
export MATOMO_HOST= export MATOMO_HOST=
@ -33,3 +31,9 @@ export PG_BACKUP_KEY=
export BACKUP_AWS_ACCESS_KEY_ID= export BACKUP_AWS_ACCESS_KEY_ID=
export BACKUP_AWS_SECRET_ACCESS_KEY= export BACKUP_AWS_SECRET_ACCESS_KEY=
export BACKUP_S3_BUCKET_NAME= export BACKUP_S3_BUCKET_NAME=
export OAUTH_CLIENT_ID=
export OAUTH_CLIENT_SECRET=
export OAUTH_ACCESS_TOKEN_URL=
export OAUTH_AUTHORIZE_URL=
export OAUTH_API_BASE_URL=
export OAUTH_LOCAL_REDIRECT_URI=

View File

@ -10,8 +10,7 @@ from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery
from basicknowledge.queries import BasicKnowledgeQuery from basicknowledge.queries import BasicKnowledgeQuery
from books.schema.mutations import BookMutations from books.schema.mutations import BookMutations
from books.schema.queries import BookQuery from books.schema.queries import BookQuery
from core.schema.mutations.coupon import CouponMutations from oauth.mutations import OauthMutations
from core.schema.mutations.main import CoreMutations
from notes.mutations import NoteMutations from notes.mutations import NoteMutations
from objectives.mutations import ObjectiveMutations from objectives.mutations import ObjectiveMutations
from objectives.schema import ObjectivesQuery from objectives.schema import ObjectivesQuery
@ -34,9 +33,9 @@ class CustomQuery(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, Objec
debug = graphene.Field(DjangoDebug, name='_debug') debug = graphene.Field(DjangoDebug, name='_debug')
class CustomMutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, class CustomMutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, OauthMutations,
ProfileMutations, SurveyMutations, NoteMutations, SpellCheckMutations, PortfolioMutations, ProfileMutations, SurveyMutations, NoteMutations, SpellCheckMutations,
CouponMutations, graphene.ObjectType): graphene.ObjectType):
if settings.DEBUG: if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='_debug') debug = graphene.Field(DjangoDebug, name='_debug')

View File

@ -4,10 +4,9 @@ from graphene_django.debug import DjangoDebug
from news.schema_public import AllNewsTeasersQuery from news.schema_public import AllNewsTeasersQuery
from users.mutations_public import UserMutations from users.mutations_public import UserMutations
from registration.mutations_public import RegistrationMutations
class PublicMutation(UserMutations, RegistrationMutations, graphene.ObjectType): class PublicMutation(UserMutations, graphene.ObjectType):
if settings.DEBUG: if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='_debug') debug = graphene.Field(DjangoDebug, name='_debug')

View File

@ -6,21 +6,20 @@ from graphene_django.views import GraphQLView
from api.schema_public import schema from api.schema_public import schema
from core.views import PrivateGraphQLView, ConfirmationKeyDisplayView from core.views import PrivateGraphQLView
app_name = 'api' app_name = 'api'
urlpatterns = [ urlpatterns = [
url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))), url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))),
url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())), url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())),
# hep proxy # oauth
url(r'^proxy/', include('registration.urls', namespace="registration")), url(r'^oauth/', include('oauth.urls', namespace="oauth")),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True, urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True,
pretty=True)))] pretty=True)))]
urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))] urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))]
urlpatterns += [url(r'^confirmation', ConfirmationKeyDisplayView.as_view(), name='confirmation_key_display')]

View File

@ -55,6 +55,7 @@ class UserFactory(factory.django.DjangoModelFactory):
first_name = factory.LazyAttribute(lambda x: fake.first_name()) first_name = factory.LazyAttribute(lambda x: fake.first_name())
last_name = factory.LazyAttribute(lambda x: fake.last_name()) last_name = factory.LazyAttribute(lambda x: fake.last_name())
email = factory.LazyAttribute(lambda x: fake.ascii_safe_email()) email = factory.LazyAttribute(lambda x: fake.ascii_safe_email())
hep_id = factory.Sequence(lambda n: n)
@factory.post_generation @factory.post_generation
def post(self, create, extracted, **kwargs): def post(self, create, extracted, **kwargs):

View File

@ -1,236 +0,0 @@
from datetime import datetime, timedelta
from django.conf import settings
import logging
import requests
logger = logging.getLogger(__name__)
TEACHER_KEY = 'teacher'
STUDENT_KEY = 'student'
MYSKILLBOX_LICENSES = {
"978-3-0355-1397-4": {
'edition': STUDENT_KEY,
'duration': 4 * 365,
'name': 'Student 4 years'
},
"978-3-0355-1860-3": {
'edition': STUDENT_KEY,
'duration': 455,
'name': 'Student 1 year'
},
"978-3-0355-1862-7": {
'edition': STUDENT_KEY,
'duration': 30,
'name': 'Student test 1 month'
},
"978-3-0355-1861-0": {
'edition': TEACHER_KEY,
'duration': 30,
'name': 'Teacher test 1 month'
},
"978-3-0355-1823-8": {
'edition': TEACHER_KEY,
'duration': 455,
'name': 'Teacher 1 year'
}
}
class HepClientException(Exception):
pass
class HepClientUnauthorizedException(Exception):
pass
class HepClient:
URL = settings.HEP_URL
WEBSITE_ID = 1
HEADERS = {
'accept': 'application/json',
'content-type': 'application/json'
}
def _call(self, url, method='get', data=None, additional_headers=None):
request_url = f'{self.URL}{url}'
if additional_headers:
headers = {**additional_headers, **self.HEADERS}
else:
headers = self.HEADERS
if method == 'post':
response = requests.post(request_url, json=data, headers=headers)
elif method == 'get':
if data:
response = requests.get(request_url, headers=headers, data=data)
else:
response = requests.get(request_url, headers=headers)
elif method == 'put':
response = requests.put(request_url, data=data)
# Todo handle 401 and most important network errors
if response.status_code == 401:
raise HepClientUnauthorizedException(response.status_code, response.json())
elif response.status_code != 200:
raise HepClientException(response.status_code, response.json())
return response
def fetch_admin_token(self, admin_user, password):
response = self._call('/rest/deutsch/V1/integration/admin/token', 'post',
data={'username': admin_user, 'password': password})
return response.content.decode('utf-8')[1:-1]
def is_email_available(self, email):
response = self._call('/rest/deutsch/V1/customers/isEmailAvailable', method='post',
data={'customerEmail': email, 'websiteId': self.WEBSITE_ID})
return response.json()
def is_email_verified(self, user_data):
return 'confirmation' not in user_data
def customer_verify_email(self, confirmation_key):
response = self._call('/rest/V1/customers/me', method='put', data={'confirmationKey': confirmation_key})
return response.json()
def customer_create(self, customer_data):
response = self._call('/rest/deutsch/V1/customers', method='post', data=customer_data)
return response.json()
def customer_token(self, username, password):
response = self._call('/rest/deutsch/V1/integration/customer/token', 'post',
data={'username': username, 'password': password})
return response.json()
def customer_me(self, token):
response = self._call('/rest/V1/customers/me', additional_headers={'authorization': f'Bearer {token}'})
return response.json()
def customer_activate(self, confirmation_key, user_id):
response = self._call(f'/customer/account/confirm/?back_url=&id={user_id}&key={confirmation_key}', method='get')
return response
def customers_search(self, admin_token, email):
response = self._call('/rest/V1/customers/search?searchCriteria[filterGroups][0][filters][0][field]='
f'email&searchCriteria[filterGroups][0][filters][0][value]={email}',
additional_headers={'authorization': f'Bearer {admin_token}'})
json_data = response.json()
if len(json_data['items']) > 0:
return json_data['items'][0]
return None
def customers_by_id(self, admin_token, user_id):
response = self._call('/rest/V1/customers/{}'.format(user_id),
additional_headers={'authorization': f'Bearer {admin_token}'})
return response.json()
def _customer_orders(self, admin_token, customer_id):
url = ('/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0]['
f'field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={customer_id}')
response = self._call(url, additional_headers={'authorization': 'Bearer {}'.format(admin_token)})
return response.json()
def coupon_redeem(self, coupon, customer_id):
try:
response = self._call(f'/rest/deutsch/V1/coupon/{coupon}/customer/{customer_id}', method='put')
except HepClientException:
return None
response_data = response.json()
if response_data[0] == '201':
return None
return response_data[0]
def myskillbox_product_for_customer(self, admin_token, customer_id):
orders = self._customer_orders(admin_token, customer_id)
products = self._extract_myskillbox_products(orders)
if len(products) == 0:
return None
else:
return self._get_relevant_product(products)
def _extract_myskillbox_products(self, orders):
products = []
for order_item in orders['items']:
status = ''
if 'status' in order_item:
status = order_item['status']
for item in order_item['items']:
order_id = -1
if 'order_id' in item:
order_id = item['order_id']
if item['sku'] in list(MYSKILLBOX_LICENSES.keys()):
product = {
'raw': item,
'activated': self._get_item_activation(order_item),
'status': status,
'order_id': order_id,
'license': MYSKILLBOX_LICENSES[item['sku']],
'isbn': item['sku']
}
products.append(product)
return products
def _get_item_activation(self, item):
if 'created_at' in item:
return datetime.strptime(item['created_at'], '%Y-%m-%d %H:%M:%S')
def _get_relevant_product(self, products):
def filter_valid_products(product):
if product['status'] != 'complete':
return False
expiry_delta = product['activated'] + timedelta(product['license']['duration'])
if HepClient.is_product_active(expiry_delta, product['isbn']):
return True
else:
return False
active_products = list(filter(filter_valid_products, products))
if len(active_products) == 0:
return None
elif len(active_products) == 1:
return active_products[0]
else:
return self._select_from_teacher_products(active_products)
def _select_from_teacher_products(self, active_products):
teacher_edition = None
# select first teacher product, as they are all valid it does not matter which one
for product in active_products:
if product['license']['edition'] == TEACHER_KEY:
teacher_edition = product
break
# select a student product, as they are all valid it does not matter which one
if not teacher_edition:
return active_products[0]
return teacher_edition
@staticmethod
def is_product_active(expiry_date, isbn):
now = datetime.now()
return expiry_date >= now >= expiry_date - timedelta(days=MYSKILLBOX_LICENSES[isbn]['duration'])

View File

@ -12,7 +12,6 @@ from wagtail.core.models import Page
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
from core.factories import UserFactory from core.factories import UserFactory
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
from users.models import Role
from users.services import create_users, create_student from users.services import create_users, create_student
from .data.module_data import data from .data.module_data import data

View File

@ -1,14 +0,0 @@
import os
import shutil
from django.core.management import BaseCommand
from core.models import AdminData
class Command(BaseCommand):
def handle(self, *args, **options):
"Update admin token via cronjob"
AdminData.objects.update_admin_token()

View File

@ -1,30 +0,0 @@
from django.conf import settings
from django.db import models
from datetime import timedelta
from django.utils import timezone
from core.hep_client import HepClient
DEFAULT_PK = 1
class AdminDataManager(models.Manager):
hep_client = HepClient()
def update_admin_token(self):
admin_token = self.hep_client.fetch_admin_token(settings.HEP_ADMIN_USER, settings.HEP_ADMIN_PASSWORD)
admin_data, created = self.get_or_create(pk=DEFAULT_PK)
admin_data.hep_admin_token = admin_token
admin_data.save()
return admin_data.hep_admin_token
def get_admin_token(self):
try:
admin_token = self.get(pk=DEFAULT_PK)
if admin_token.updated_at < timezone.now() + timedelta(hours=1):
admin_token = self.update_admin_token()
except self.model.DoesNotExist:
admin_token = self.update_admin_token()
return admin_token

View File

@ -1,12 +1,10 @@
import json
import re import re
from django.conf import settings from django.conf import settings
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponse from django.http import Http404, HttpResponsePermanentRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from core.utils import is_private_api_call_allowed
try: try:
from threading import local from threading import local
@ -99,12 +97,3 @@ class UserLoggedInCookieMiddleWare(MiddlewareMixin):
response.delete_cookie(self.cookie_name) response.delete_cookie(self.cookie_name)
return response return response
class UserHasLicenseMiddleWare(MiddlewareMixin):
def process_response(self, request, response):
if request.path == '/api/graphql/':
if not is_private_api_call_allowed(request.user, request.body):
return HttpResponse(json.dumps({'errors': ['no active license']}), status=402)
return response

View File

@ -1,12 +0,0 @@
from datetime import datetime
from django.db import models
from core.managers import AdminDataManager
class AdminData(models.Model):
hep_admin_token = models.CharField(max_length=100, blank=False, null=False)
updated_at = models.DateTimeField(blank=False, null=True, auto_now=True)
objects = AdminDataManager()

View File

@ -1,44 +0,0 @@
import graphene
from graphene import relay
from core.hep_client import HepClient, HepClientException
from users.user_signup_login_handler import check_and_create_licenses, create_role_for_user
class Coupon(relay.ClientIDMutation):
class Input:
coupon_code = graphene.String()
success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
coupon_code = kwargs.get('coupon_code').strip()
hep_client = HepClient()
try:
hep_id = info.context.user.hep_id
except AttributeError:
raise Exception('not_authenticated')
try:
response = hep_client.coupon_redeem(coupon_code, hep_id)
except HepClientException:
raise Exception('unknown_error')
if not response:
raise Exception('invalid_coupon')
license, error_msg = check_and_create_licenses(hep_client, info.context.user)
# todo fail if no license
if error_msg:
raise Exception(error_msg)
create_role_for_user(info.context.user, license.for_role.key)
return cls(success=True)
class CouponMutations:
redeem_coupon = Coupon.Field()

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2018 ITerativ GmbH. All rights reserved.
#
# Created on 22.10.18
# @author: chrigu <christian.cueni@iterativ.ch>
import graphene
from django.contrib.auth import logout
class Logout(graphene.Mutation):
success = graphene.Boolean()
def mutate(self, info, **kwargs):
try:
logout(info.context)
return Logout(success=True)
except Exception:
return Logout(success=False)

View File

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2018 ITerativ GmbH. All rights reserved.
#
# Created on 22.10.18
# @author: chrigu <christian.cueni@iterativ.ch>
from core.schema.mutations.coupon import Coupon
from core.schema.mutations.logout import Logout
class CoreMutations(object):
logout = Logout.Field()
coupon = Coupon.Field()

View File

@ -59,8 +59,8 @@ INSTALLED_APPS = [
'statistics', 'statistics',
'surveys', 'surveys',
'notes', 'notes',
'registration',
'news', 'news',
'oauth',
'wagtail.contrib.forms', 'wagtail.contrib.forms',
'wagtail.contrib.redirects', 'wagtail.contrib.redirects',
@ -131,7 +131,7 @@ MIDDLEWARE += [
'core.middleware.ThreadLocalMiddleware', 'core.middleware.ThreadLocalMiddleware',
'core.middleware.CommonRedirectMiddleware', 'core.middleware.CommonRedirectMiddleware',
'core.middleware.UserLoggedInCookieMiddleWare', 'core.middleware.UserLoggedInCookieMiddleWare',
'core.middleware.UserHasLicenseMiddleWare', 'oauth.middleware.user_has_license_middleware',
] ]
ROOT_URLCONF = 'core.urls' ROOT_URLCONF = 'core.urls'
@ -397,10 +397,29 @@ EMAIL_USE_SSL = False
ALLOW_BETA_LOGIN = True ALLOW_BETA_LOGIN = True
# HEP # HEP
HEP_ADMIN_USER = os.environ.get("HEP_ADMIN_USER")
HEP_ADMIN_PASSWORD = os.environ.get("HEP_ADMIN_PASSWORD")
HEP_URL = os.environ.get("HEP_URL") HEP_URL = os.environ.get("HEP_URL")
HEP_MYSKILLBOX_GROUP_ID = 5
# HEP Oauth
AUTHLIB_OAUTH_CLIENTS = {
'hep': {
'client_id': os.environ.get("OAUTH_CLIENT_ID"),
'client_secret': os.environ.get("OAUTH_CLIENT_SECRET"),
'request_token_url': None,
'request_token_params': None,
'access_token_url': os.environ.get("OAUTH_ACCESS_TOKEN_URL"),
'access_token_params': None,
'refresh_token_url': None,
'authorize_url': os.environ.get("OAUTH_AUTHORIZE_URL"),
'api_base_url': os.environ.get("OAUTH_API_BASE_URL"),
'client_kwargs': {
'scope': 'orders',
'token_endpoint_auth_method': 'client_secret_post',
'token_placement': 'header',
}
}
}
OAUTH_LOCAL_REDIRECT_URI = os.environ.get("OAUTH_LOCAL_REDIRECT_URI")
TASKBASE_USER = os.environ.get("TASKBASE_USER") TASKBASE_USER = os.environ.get("TASKBASE_USER")
TASKBASE_PASSWORD = os.environ.get("TASKBASE_PASSWORD") TASKBASE_PASSWORD = os.environ.get("TASKBASE_PASSWORD")

View File

@ -1,40 +0,0 @@
{
"id": 49124,
"group_id": 1,
"default_billing": "47579",
"default_shipping": "47579",
"created_at": "2018-07-19 15:05:27",
"updated_at": "2019-11-26 17:04:29",
"created_in": "hep verlag",
"email": "1heptest19072018@mailinator.com",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"gender": 2,
"store_id": 1,
"website_id": 1,
"addresses": [
{
"id": 47579,
"customer_id": 49124,
"region": {
"region_code": null,
"region": null,
"region_id": 0
},
"region_id": 0,
"country_id": "CH",
"street": [
"Test"
],
"telephone": "",
"postcode": "0000",
"city": "Test",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"default_shipping": true,
"default_billing": true
}
]
}

View File

@ -1,526 +0,0 @@
{
"items": [
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 46,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 44.88,
"base_subtotal_incl_tax": 46,
"base_tax_amount": 1.12,
"base_total_due": 46,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83693,
"created_at": "2018-07-19 15:05:33",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57612,
"global_currency_code": "CHF",
"grand_total": 46,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614768",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "71aedb",
"quote_id": 104401,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 44.88,
"subtotal_incl_tax": 46,
"tax_amount": 1.12,
"total_due": 46,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:05:33",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Myskillbox Schüler Edition",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1397-4",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83693,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57612,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 46,
"base_amount_ordered": 46,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57612,
"method": "checkmo",
"parent_id": 57612,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244885,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "licence-coupon \"ebf81a59b968\"",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244884,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:05:33",
"entity_id": 244883,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244882,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1082-9",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
]
}
]
}
},
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 24,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 23.41,
"base_subtotal_incl_tax": 24,
"base_tax_amount": 0.59,
"base_total_due": 24,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83696,
"created_at": "2018-07-19 15:19:00",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57614,
"global_currency_code": "CHF",
"grand_total": 24,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614770",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "1a88e9",
"quote_id": 104403,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 23.41,
"subtotal_incl_tax": 24,
"tax_amount": 0.59,
"total_due": 24,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:19:00",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83696,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57614,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 24,
"base_amount_ordered": 24,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57614,
"method": "checkmo",
"parent_id": 57614,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244890,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "licence-coupon \"ece5e74a2b36\"",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244889,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:19:00",
"entity_id": 244888,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244887,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
]
}
]
}
}
],
"search_criteria": {
"filter_groups": [
{
"filters": [
{
"field": "customer_id",
"value": "49124",
"condition_type": "eq"
}
]
}
]
},
"total_count": 2
}

View File

@ -1,526 +0,0 @@
{
"items": [
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 46,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 44.88,
"base_subtotal_incl_tax": 46,
"base_tax_amount": 1.12,
"base_total_due": 46,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83693,
"created_at": "2018-07-19 15:05:33",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57612,
"global_currency_code": "CHF",
"grand_total": 46,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614768",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "71aedb",
"quote_id": 104401,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 44.88,
"subtotal_incl_tax": 46,
"tax_amount": 1.12,
"total_due": 46,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:05:33",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Myskillbox Lehreredition",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1823-8",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83693,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57612,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 46,
"base_amount_ordered": 46,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57612,
"method": "checkmo",
"parent_id": 57612,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244885,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "licence-coupon \"ebf81a59b968\"",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244884,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:05:33",
"entity_id": 244883,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244882,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1082-9",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
]
}
]
}
},
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 24,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 23.41,
"base_subtotal_incl_tax": 24,
"base_tax_amount": 0.59,
"base_total_due": 24,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83696,
"created_at": "2018-07-19 15:19:00",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57614,
"global_currency_code": "CHF",
"grand_total": 24,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614770",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "1a88e9",
"quote_id": 104403,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 23.41,
"subtotal_incl_tax": 24,
"tax_amount": 0.59,
"total_due": 24,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:19:00",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83696,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57614,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 24,
"base_amount_ordered": 24,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57614,
"method": "checkmo",
"parent_id": 57614,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244890,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "licence-coupon \"ece5e74a2b36\"",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244889,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:19:00",
"entity_id": 244888,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244887,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
]
}
]
}
}
],
"search_criteria": {
"filter_groups": [
{
"filters": [
{
"field": "customer_id",
"value": "49124",
"condition_type": "eq"
}
]
}
]
},
"total_count": 2
}

View File

@ -16,11 +16,10 @@ class ApiAccessTestCase(TestCase):
def test_graphqlEndpoint_shouldNotBeAccessibleWithoutLogin(self): def test_graphqlEndpoint_shouldNotBeAccessibleWithoutLogin(self):
c = Client() c = Client()
response = c.post('/api/graphql/', data=self.query, content_type='application/json') response = c.post('/api/graphql/', data=self.query, content_type='application/json')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 402)
self.assertEqual(response.url, '/login?next=/api/graphql/')
def test_graphqlEndpoint_shouldBeAccessibleWithLogin(self): def test_graphqlEndpoint_shouldBeAccessibleForSuperUser(self):
UserFactory(username='admin') UserFactory(username='admin', is_staff=True, is_active=True, is_superuser=True)
c = Client() c = Client()
c.login(username='admin', password='test') c.login(username='admin', password='test')

View File

@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 03.02.20
# @author: chrigu <christian.cueni@iterativ.ch>
from unittest.mock import patch
import requests
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.factories import UserFactory
from core.hep_client import HepClient
from core.tests.mock_hep_data_factory import MockResponse, VALID_TEACHERS_ORDERS
from users.models import License, Role, SchoolClass, UserRole
class CouponTests(TestCase):
def setUp(self):
Role.objects.create_default_roles()
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch', hep_id=3)
Role.objects.create_default_roles()
self.teacher_role = Role.objects.get_default_teacher_role()
# adding session
request = RequestFactory().post('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.user = self.user
request.session.save()
self.client = Client(schema=schema, context_value=request)
def make_coupon_mutation(self, coupon_code, client):
mutation = '''
mutation Coupon($input: CouponInput!){
coupon(input: $input) {
success
}
}
'''
return client.execute(mutation, variables={
'input': {
'couponCode': coupon_code
}
})
@patch.object(requests, 'put', return_value=MockResponse(200, data=['200', 'Coupon successfully redeemed']))
@patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS)
@patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'})
def test_user_has_valid_coupon(self, admin_mock, orders_mock, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
user_role_key = self.user.user_roles.get(user=self.user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=self.user)
self.assertIsNotNone(license)
school_class = SchoolClass.objects.get(users__in=[self.user])
self.assertIsNotNone(school_class)
self.assertTrue(result.get('data').get('coupon').get('success'))
self.assertTrue(self.user.is_authenticated)
@patch.object(requests, 'put', return_value=MockResponse(200, data=['201', 'Invalid Coupon']))
def test_user_has_invalid_coupon(self, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
self.assertEqual(result.get('errors')[0].get('message'), 'invalid_coupon')
@patch.object(requests, 'put', return_value=MockResponse(200, data=['201', 'Invalid Coupon']))
def test_unauthenticated_user_cannot_redeem(self, response_mock):
request = RequestFactory().post('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
client = Client(schema=schema, context_value=request)
result = self.make_coupon_mutation('COUPON--1234', client)
self.assertEqual(result.get('errors')[0].get('message'), 'not_authenticated')

View File

@ -24,7 +24,8 @@ urlpatterns = [
url(r'^api/', include('api.urls', namespace="api")), url(r'^api/', include('api.urls', namespace="api")),
#favicon #favicon
url(r'^favicon\.ico$', RedirectView.as_view(url='/static/favicon@2x.png', permanent=True)) url(r'^favicon\.ico$', RedirectView.as_view(url='/static/favicon@2x.png', permanent=True)),
] ]
if settings.DEBUG and not settings.USE_AWS: if settings.DEBUG and not settings.USE_AWS:

View File

@ -28,17 +28,15 @@ def is_private_api_call_allowed(user, body):
# logged in users should only be able to access all resources if they have a valid license # logged in users should only be able to access all resources if they have a valid license
# logged in users without valid license have only access to logout, me & coupon mutations # logged in users without valid license have only access to logout, me & coupon mutations
body_unicode = body.decode('utf-8') if user.is_anonymous:
return False
try: if user.is_superuser:
if not user.hep_id:
return True
except AttributeError:
return True return True
# logout, me and coupon resources are always allowed. Even if the user has no valid license body_unicode = body.decode('utf-8')
if re.search(r"mutation\s*.*\s*logout\s*{", body_unicode) or re.search(r"query\s*.*\s*me\s*{", body_unicode) \
or re.search(r"mutation\s*Coupon", body_unicode): if is_endpoint_allowed(body_unicode):
return True return True
license_expiry = user.license_expiry_date license_expiry = user.license_expiry_date
@ -50,6 +48,12 @@ def is_private_api_call_allowed(user, body):
return True return True
# logout, betalogin, me and coupon resources are always allowed. Even if the user has no valid license
def is_endpoint_allowed(body):
return re.search(r"mutation\s*.*\s*logout\s*{", body) or re.search(r"query\s*.*\s*me\s*{", body) \
or re.search(r"mutation\s*Coupon", body) or re.search(r"mutation\s*BetaLogin", body)
def sync_hidden_for(model, school_class_template, school_class_to_sync): def sync_hidden_for(model, school_class_template, school_class_to_sync):
if model.hidden_for.filter(id=school_class_template.id).exists() and not model.hidden_for.filter( if model.hidden_for.filter(id=school_class_template.id).exists() and not model.hidden_for.filter(
id=school_class_to_sync.id).exists(): id=school_class_to_sync.id).exists():

View File

@ -7,9 +7,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import TemplateView from django.views.generic import TemplateView
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
from core.hep_client import HepClient
from core.models import AdminData
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
pass pass
@ -27,20 +24,3 @@ def home(request):
print('Can not connect to dev server at http://localhost:8080:', e) print('Can not connect to dev server at http://localhost:8080:', e)
return render(request, 'index.html', {}) return render(request, 'index.html', {})
class ConfirmationKeyDisplayView(TemplateView):
template_name = 'confirmation_key.html'
def get_context_data(self, *args, **kwargs):
email = self.request.GET.get('email', '')
hep_client = HepClient()
admin_token = AdminData.objects.get_admin_token()
hep_user = hep_client.customers_search(admin_token, email)
context = super().get_context_data(**kwargs)
context['confirmation_key'] = hep_user['confirmation']
context['hep_id'] = hep_user['id']
return context

6
server/oauth/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class UserConfig(AppConfig):
name = 'oauth'

21
server/oauth/factories.py Normal file
View File

@ -0,0 +1,21 @@
import time
from datetime import timedelta
import factory
from django.utils import timezone
from oauth.models import OAuth2Token
IN_A_HOUR = timezone.now() + timedelta(hours=1)
IN_A_HOUR_UNIX = int(time.mktime(IN_A_HOUR.timetuple()))
class Oauth2TokenFactory(factory.django.DjangoModelFactory):
class Meta:
model = OAuth2Token
token_type = 'Bearer'
access_token = 'asdfgghh'
refresh_token = 'yxcvbnnm'
expires_at = IN_A_HOUR_UNIX

193
server/oauth/hep_client.py Normal file
View File

@ -0,0 +1,193 @@
import requests
from django.conf import settings
from django.utils.dateparse import parse_datetime
from datetime import timedelta
from oauth.models import OAuth2Token
from oauth.oauth_client import oauth, fetch_token
from users.licenses import MYSKILLBOX_LICENSES, is_myskillbox_product, TEACHER_KEY
from users.models import License
from core.logger import get_logger
logger = get_logger(__name__)
VALID_PRODUCT_STATES = ['waiting', 'paid', 'completed', 'shipped']
class HepClientException(Exception):
pass
class HepClientUnauthorizedException(Exception):
pass
class HepClientNoTokenException(Exception):
pass
class HepClient:
def _call(self, url, token_dict, method='get', data=None):
headers = {"accept": "application/json"}
if method == 'post':
response = oauth.hep.post(url, json=data, token=token_dict, headers=headers)
elif method == 'get':
response = oauth.hep.get(url, params=data, token=token_dict, headers=headers)
elif method == 'put':
response = oauth.hep.put(url, data=data, token=token_dict, headers=headers)
return self._handle_response(response)
def _handle_response(self, response):
if response.status_code == 401:
raise HepClientUnauthorizedException(response.status_code, response.json())
elif response.status_code != 200:
logger.warning(f'Hepclient error: Received {response.status_code} {response.json()}')
raise HepClientException(response.status_code, response.json())
return response
def _get_valid_token(self, request, token_dict):
if request is None and token_dict is None:
raise HepClientNoTokenException
if not token_dict:
token_dict = fetch_token('', request)
if not token_dict:
raise HepClientNoTokenException
if OAuth2Token.is_dict_expired(token_dict):
refresh_data = self._refresh_token(token_dict)
token, refresh_success = OAuth2Token.update_dict_with_refresh_data(refresh_data, token_dict['access_token'])
if not refresh_success:
raise HepClientUnauthorizedException
token_dict = token.to_token()
return token_dict
def _refresh_token(self, token_dict):
data = {
'grant_type': 'refresh_token',
'refresh_token': token_dict['refresh_token'],
'client_id': settings.AUTHLIB_OAUTH_CLIENTS['hep']['client_id'],
'client_secret': settings.AUTHLIB_OAUTH_CLIENTS['hep']['client_secret'],
'scope': ''
}
response = requests.post(f'{settings.AUTHLIB_OAUTH_CLIENTS["hep"]["api_base_url"]}/oauth/token', json=data)
return self._handle_response(response).json()
def is_email_verified(self, user_data):
return user_data['email_verified_at'] is not None
def user_details(self, request=None, token_dict=None):
token_dict = self._get_valid_token(request, token_dict)
response = self._call('api/auth/user', token_dict)
return response.json()['data']
def logout(self, request=None, token_dict=None):
token_dict = self._get_valid_token(request, token_dict)
self._call('api/auth/logout', token_dict, method='post')
return True
def fetch_eorders(self, request=None, token_dict=None):
token_dict = self._get_valid_token(request, token_dict)
data = {
'filters[product_type]': 'eLehrmittel',
}
response = self._call('api/partners/users/orders/search', token_dict, data=data)
return response.json()['data']
def active_myskillbox_product_for_customer(self, request=None, token_dict=None):
eorders = self.fetch_eorders(request=request, token_dict=token_dict)
myskillbox_products = self._extract_myskillbox_products(eorders)
if len(myskillbox_products) == 0:
return None
else:
return self._get_active_product(myskillbox_products)
def redeem_coupon(self, coupon_code, customer_id, request=None, token_dict=None):
token_dict = self._get_valid_token(request, token_dict)
try:
response = self._call(f'api/partners/users/{customer_id}/coupons/redeem', token_dict, method='post',
data={'code': coupon_code})
except HepClientException:
return None
response_data = response.json()
return response_data
def _extract_myskillbox_products(self, eorders):
products = []
for eorder in eorders:
if 'items' not in eorder:
continue
status = eorder.get('status', '')
for entry in eorder['items']:
product = self.entry_to_product(entry, self._get_item_activation(eorder), status)
if product:
products.append(product)
return products
def entry_to_product(self, entry, activation_date, status):
if is_myskillbox_product(entry['isbn']) and activation_date:
return {
'raw': entry,
'activated': activation_date,
'status': status,
'order_id': entry['id'],
'license': MYSKILLBOX_LICENSES[entry['isbn']],
'isbn': entry['isbn']
}
return None
def _get_item_activation(self, eorder):
if 'created_at' in eorder:
return parse_datetime(eorder['created_at'])
else:
return None
def _get_active_product(self, products):
def filter_valid_products(product):
if product['status'] not in VALID_PRODUCT_STATES:
return False
expiry_delta = product['activated'] + timedelta(product['license']['duration'])
return License.is_product_active(expiry_delta, product['isbn'])
active_products = list(filter(filter_valid_products, products))
if len(active_products) == 0:
return None
elif len(active_products) == 1:
return active_products[0]
else:
return self._select_from_teacher_products(active_products)
def _select_from_teacher_products(self, active_products):
# select first teacher product, as they are all valid it does not matter which one
for product in active_products:
if product['license']['edition'] == TEACHER_KEY:
return product
# select a student product, as they are all valid it does not matter which one
return active_products[0]

23
server/oauth/managers.py Normal file
View File

@ -0,0 +1,23 @@
from django.db import models
class OAuth2TokenManager(models.Manager):
def update_or_create_token(self, token_data, user):
query = self.filter(user=user)
token_properties = {
'token_type': token_data['token_type'],
'access_token': token_data['access_token'],
'refresh_token': token_data['refresh_token'],
'expires_at': token_data['expires_at'],
}
if query.exists():
return query.update(**token_properties)
else:
return self._create_oauthtoken(user, token_properties)
def _create_oauthtoken(self, user, token_properties):
token = self.create(user=user, **token_properties)
return token

View File

@ -0,0 +1,16 @@
import json
from django.http import HttpResponse
from core.utils import is_private_api_call_allowed
def user_has_license_middleware(get_response):
def middleware(request):
if request.path == '/api/graphql/':
if not is_private_api_call_allowed(request.user, request.body):
return HttpResponse(json.dumps({'errors': ['no active license']}), status=402)
return get_response(request)
return middleware

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.21 on 2021-05-12 14:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='OAuth2Token',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token_type', models.CharField(max_length=40)),
('access_token', models.TextField()),
('refresh_token', models.TextField()),
('expires_at', models.PositiveIntegerField()),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

77
server/oauth/models.py Normal file
View File

@ -0,0 +1,77 @@
# https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients
import base64
import json
from time import mktime
from django.contrib.auth import get_user_model
from django.db import models
from django.utils import timezone
from oauth.managers import OAuth2TokenManager
from core.logger import get_logger
logger = get_logger(__name__)
class OAuth2Token(models.Model):
token_type = models.CharField(max_length=40)
access_token = models.TextField()
refresh_token = models.TextField()
expires_at = models.PositiveIntegerField()
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
objects = OAuth2TokenManager()
@classmethod
def is_dict_expired(cls, token_dict):
return cls.has_timestamp_expired(token_dict['expires_at'])
@classmethod
def has_timestamp_expired(cls, expires_at):
now = timezone.now()
now_unix_timestamp = int(mktime(now.timetuple()))
return expires_at < now_unix_timestamp
@classmethod
def update_dict_with_refresh_data(cls, data, old_access_token):
try:
token = cls.objects.get(access_token=old_access_token)
except cls.DoesNotExist:
return False
return token.update_with_refresh_data(data)
def to_token(self):
return dict(
access_token=self.access_token,
token_type=self.token_type,
refresh_token=self.refresh_token,
expires_at=self.expires_at,
)
def has_expired(self):
return OAuth2Token.has_timestamp_expired(self.expires_at)
def update_with_refresh_data(self, data):
payload = self._jwt_payload(data['access_token'])
if not payload:
return None, False
self.token_type = data['token_type']
self.access_token = data['access_token']
self.refresh_token = data['refresh_token']
self.expires_at = int(payload['exp'])
self.save()
return self, True
def _jwt_payload(self, jwt):
jwt_parts = jwt.split('.')
try:
payload_bytes = base64.b64decode(jwt_parts[1])
payload = json.loads(payload_bytes.decode("UTF-8"))
except Exception as e:
logger.warning(f'OAuthToken error: Could not decode jwt: {e}')
return None
return payload

79
server/oauth/mutations.py Normal file
View File

@ -0,0 +1,79 @@
import graphene
from django.contrib.auth import logout
from django.utils.timezone import now
from graphene import relay
from oauth.hep_client import HepClient, HepClientException
from oauth.models import OAuth2Token
from oauth.user_signup_login_handler import create_role_for_user
from users.models import License
class Coupon(relay.ClientIDMutation):
class Input:
coupon_code = graphene.String()
success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
coupon_code = kwargs.get('coupon_code').strip()
hep_client = HepClient()
try:
hep_id = info.context.user.hep_id
except AttributeError:
raise Exception('not_authenticated')
try:
response = hep_client.redeem_coupon(coupon_code, hep_id, request=info.context)
except HepClientException:
raise Exception('unknown_error')
if not response:
raise Exception('invalid_coupon')
product = hep_client.entry_to_product(response['data'], now(), 'coupon')
if not product:
raise Exception('non_myskillbox_product')
license = License.objects.create_license_for_role(info.context.user, product['activated'], product['raw'],
product['license']['edition'],
product['order_id'], product['isbn'])
create_role_for_user(info.context.user, license.for_role.key)
return cls(success=True)
class CouponMutations:
redeem_coupon = Coupon.Field()
class Logout(graphene.Mutation):
success = graphene.Boolean()
def mutate(self, info):
# log user out from myskillbox even if hep logout fails
try:
token = OAuth2Token.objects.get(user=info.context.user)
hep_client = HepClient()
try:
hep_client.logout(request=info.context)
except Exception as e:
pass
token.delete()
except OAuth2Token.DoesNotExist:
pass
logout(info.context)
return Logout(success=True)
class OauthMutations(object):
logout = Logout.Field()
coupon = Coupon.Field()

View File

@ -0,0 +1,32 @@
from authlib.integrations.django_client import OAuth
from django.conf import settings
from oauth.models import OAuth2Token
# https://docs.authlib.org/en/latest/client/frameworks.html#frameworks-clients
def fetch_token(name, request):
try:
token = OAuth2Token.objects.get(
user=request.user
)
return token.to_token()
except (OAuth2Token.DoesNotExist, TypeError):
return None
oauth = OAuth(fetch_token=fetch_token)
oauth.register(
name='hep',
client_id=settings.AUTHLIB_OAUTH_CLIENTS['hep']['client_id'],
client_secret=settings.AUTHLIB_OAUTH_CLIENTS['hep']['client_secret'],
request_token_url=settings.AUTHLIB_OAUTH_CLIENTS['hep']['request_token_url'],
request_token_params=None,
access_token_url=settings.AUTHLIB_OAUTH_CLIENTS['hep']['access_token_url'],
access_token_params=None,
authorize_url=settings.AUTHLIB_OAUTH_CLIENTS['hep']['authorize_url'],
authorize_params=None,
api_base_url=settings.AUTHLIB_OAUTH_CLIENTS['hep']['api_base_url'],
client_kwargs=settings.AUTHLIB_OAUTH_CLIENTS['hep']['client_kwargs'],
)

View File

@ -0,0 +1,48 @@
[
{
"id": 5,
"status": "paid",
"order_total": 10000,
"created_at": "2021-05-10T20:52:16.000000Z",
"billing_address": {
"id": 20,
"salutation": "male",
"first_name": "Hans",
"last_name": "Meier",
"company": "Lustig GmbH",
"street": "Sulgenrain 12890",
"city": "Bern",
"post_code": "3007",
"country": "Schweiz"
},
"delivery_address": {
"id": 19,
"salutation": "male",
"first_name": "Hans",
"last_name": "Muster",
"company": "Muster AG",
"street": "Ruderweg 24",
"city": "Bern",
"post_code": "3000",
"country": "Schweiz"
},
"entries": [
{
"id": 3433,
"uri": "\/products\/myskillbox-lernende",
"url": null,
"title": "mySkillbox für Lernende ",
"subtitle": "Lizenz gültig für 4 Jahre",
"isbn": "978-3-0355-1397-4",
"slug": "myskillbox-lernende",
"product_type": "eLehrmittel",
"product_form": "",
"cover": "https:\/\/hep-verlag.fra1.digitaloceanspaces.com\/staging\/products\/2921\/978-3-0355-1397-4.jpg",
"price": 100,
"price_total": 100,
"amount": 1,
"authors": []
}
]
}
]

View File

@ -0,0 +1,148 @@
from unittest.mock import patch
from authlib.integrations.base_client import BaseApp
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.factories import UserFactory
from oauth.factories import Oauth2TokenFactory
from users.tests.mock_hep_data_factory import MockResponse
from users.models import License, Role, SchoolClass
REDEEM_MYSKILLBOX_SUCCESS_RESPONSE = {
"data": {
"id": 3433,
"uri": "\/products\/myskillbox-lehrpersonen",
"url": None,
"title": "mySkillbox für Lehrpersonen ",
"subtitle": "Lizenz gültig für 1 Jahr",
"isbn": "978-3-0355-1861-0",
"slug": "myskillbox-lehrpersonen",
"product_type": "eLehrmittel",
"product_form": "",
"cover": "https:\/\/hep-verlag.fra1.digitaloceanspaces.com\/staging\/products\/2921\/978-3-0355-1861-0.jpg",
"price": 100,
"price_total": 100,
"amount": 1,
"authors": []
}
}
REDEEM_OTHER_LICENSE_RESPONSE = {
"data": {
"id": 3433,
"uri": "\/products\/someothe",
"url": None,
"title": "Ein e-Lehrmittel",
"subtitle": "Lizenz gültig für 1 Jahr",
"isbn": "111-2-3333-4444-0",
"slug": "some-other",
"product_type": "eLehrmittel",
"product_form": "",
"cover": "https:\/\/hep-verlag.fra1.digitaloceanspaces.com\/staging\/products\/2921\/978-3-0355-123.jpg",
"price": 100,
"price_total": 100,
"amount": 1,
"authors": []
}
}
INVALID_LICENSE = {
"message": "The given data was invalid.",
"errors": {
"code": [
"The coupons was already redeemed."
]
}
}
class CouponTests(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch', hep_id=3)
Role.objects.create_default_roles()
self.teacher_role = Role.objects.get_default_teacher_role()
Oauth2TokenFactory(user=self.user)
# adding session
request = RequestFactory().post('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.user = self.user
request.session.save()
self.client = Client(schema=schema, context_value=request)
def make_coupon_mutation(self, coupon_code, client):
mutation = '''
mutation Coupon($input: CouponInput!){
coupon(input: $input) {
success
}
}
'''
return client.execute(mutation, variables={
'input': {
'couponCode': coupon_code
}
})
@patch.object(BaseApp, 'post', return_value=MockResponse(200, data=REDEEM_MYSKILLBOX_SUCCESS_RESPONSE))
def test_user_has_valid_skillbox_coupon(self, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
user_role = self.user.user_roles.get(user=self.user)
self.assertEqual(user_role.role.key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=self.user)
self.assertIsNotNone(license)
school_class = SchoolClass.objects.get(users__in=[self.user])
self.assertIsNotNone(school_class)
self.assertTrue(result.get('data').get('coupon').get('success'))
self.assertTrue(self.user.is_authenticated)
@patch.object(BaseApp, 'post', return_value=MockResponse(200, data=REDEEM_OTHER_LICENSE_RESPONSE))
def test_user_has_valid_non_skillbox_coupon(self, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
try:
self.user.user_roles.get(user=self.user)
self.fail("CouponTests.test_user_has_valid_non_skillbox_coupon: Should not have created user role")
except:
pass
try:
License.objects.get(licensee=self.user)
self.fail("CouponTests.test_user_has_valid_non_skillbox_coupon: License should not exist")
except License.DoesNotExist:
pass
self.assertEqual(result.get('errors')[0].get('message'), 'non_myskillbox_product')
self.assertTrue(self.user.is_authenticated)
@patch.object(BaseApp, 'post', return_value=MockResponse(404, data=INVALID_LICENSE))
def test_user_has_invalid_coupon(self, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
self.assertEqual(result.get('errors')[0].get('message'), 'invalid_coupon')
@patch.object(BaseApp, 'post', return_value=MockResponse(422, data=INVALID_LICENSE))
def test_user_has_already_used_coupon(self, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
self.assertEqual(result.get('errors')[0].get('message'), 'invalid_coupon')
@patch.object(BaseApp, 'put', return_value=MockResponse(200, data=['201', 'Invalid Coupon']))
def test_unauthenticated_user_cannot_redeem(self, response_mock):
request = RequestFactory().post('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
client = Client(schema=schema, context_value=request)
result = self.make_coupon_mutation('COUPON--1234', client)
self.assertEqual(result.get('errors')[0].get('message'), 'not_authenticated')

View File

@ -1,8 +1,17 @@
import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch
import requests
from django.test import TestCase from django.test import TestCase
from core.hep_client import HepClient, MYSKILLBOX_LICENSES
from core.factories import UserFactory
from oauth.factories import Oauth2TokenFactory
from oauth.hep_client import HepClient, HepClientUnauthorizedException, HepClientNoTokenException, HepClientException
from oauth.models import OAuth2Token
from oauth.tests.test_oauth2token import REFRESH_DATA
from users.licenses import MYSKILLBOX_LICENSES
from users.models import License
from users.tests.mock_hep_data_factory import MockResponse
ISBNS = list(MYSKILLBOX_LICENSES.keys()) ISBNS = list(MYSKILLBOX_LICENSES.keys())
@ -18,6 +27,12 @@ class HepClientTestCases(TestCase):
self.hep_client = HepClient() self.hep_client = HepClient()
self.now = datetime.now() self.now = datetime.now()
def _create_token(self):
user = UserFactory(username="bert")
token = Oauth2TokenFactory(user=user)
self.token_dict = token.to_token()
return self.token_dict
def test_has_no_valid_product(self): def test_has_no_valid_product(self):
products = [ products = [
{ {
@ -25,25 +40,25 @@ class HepClientTestCases(TestCase):
'isbn': TEACHER_ISBN, 'isbn': TEACHER_ISBN,
'raw': {}, 'raw': {},
'activated': self.now - timedelta(2*TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(2*TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
'isbn': TEACHER_ISBN, 'isbn': TEACHER_ISBN,
'raw': {}, 'raw': {},
'activated': self.now - timedelta(3 * TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(3 * TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
'isbn': TEACHER_ISBN, 'isbn': TEACHER_ISBN,
'raw': {}, 'raw': {},
'activated': self.now - timedelta(4 * TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(4 * TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
} }
] ]
relevant_product = self.hep_client._get_relevant_product(products) relevant_product = self.hep_client._get_active_product(products)
self.assertIsNone(relevant_product) self.assertIsNone(relevant_product)
def test_has_no_not_completed_product(self): def test_has_no_not_completed_product(self):
@ -57,7 +72,7 @@ class HepClientTestCases(TestCase):
} }
] ]
relevant_product = self.hep_client._get_relevant_product(products) relevant_product = self.hep_client._get_active_product(products)
self.assertIsNone(relevant_product) self.assertIsNone(relevant_product)
def test_has_valid_product(self): def test_has_valid_product(self):
@ -69,7 +84,7 @@ class HepClientTestCases(TestCase):
'id': 0 'id': 0
}, },
'activated': self.now - timedelta(7), 'activated': self.now - timedelta(7),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
@ -78,7 +93,7 @@ class HepClientTestCases(TestCase):
'id': 1 'id': 1
}, },
'activated': self.now - timedelta(3 * TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(3 * TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
@ -87,11 +102,11 @@ class HepClientTestCases(TestCase):
'id': 2 'id': 2
}, },
'activated': self.now - timedelta(4 * TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(4 * TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
} }
] ]
relevant_product = self.hep_client._get_relevant_product(products) relevant_product = self.hep_client._get_active_product(products)
self.assertEqual(relevant_product['raw']['id'], 0) self.assertEqual(relevant_product['raw']['id'], 0)
def test_has_multiple_valid_teacher_products_but_only_one_active(self): def test_has_multiple_valid_teacher_products_but_only_one_active(self):
@ -103,7 +118,7 @@ class HepClientTestCases(TestCase):
'id': 0 'id': 0
}, },
'activated': self.now - timedelta(7), 'activated': self.now - timedelta(7),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
@ -112,7 +127,7 @@ class HepClientTestCases(TestCase):
'id': 1 'id': 1
}, },
'activated': self.now - timedelta(3 * TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(3 * TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
@ -121,11 +136,11 @@ class HepClientTestCases(TestCase):
'id': 2 'id': 2
}, },
'activated': self.now - timedelta(4 * TEACHER_LICENSE['duration']), 'activated': self.now - timedelta(4 * TEACHER_LICENSE['duration']),
'status': 'complete' 'status': 'paid'
} }
] ]
relevant_product = self.hep_client._get_relevant_product(products) relevant_product = self.hep_client._get_active_product(products)
self.assertEqual(relevant_product['raw']['id'], 0) self.assertEqual(relevant_product['raw']['id'], 0)
def test_has_valid_student_and_teacher_edition(self): def test_has_valid_student_and_teacher_edition(self):
@ -137,7 +152,7 @@ class HepClientTestCases(TestCase):
'id': 0 'id': 0
}, },
'activated': self.now - timedelta(7), 'activated': self.now - timedelta(7),
'status': 'complete' 'status': 'paid'
}, },
{ {
'license': TEACHER_LICENSE, 'license': TEACHER_LICENSE,
@ -146,7 +161,7 @@ class HepClientTestCases(TestCase):
'id': 1 'id': 1
}, },
'activated': self.now - timedelta(7), 'activated': self.now - timedelta(7),
'status': 'complete' 'status': 'paid'
} }
] ]
@ -157,12 +172,58 @@ class HepClientTestCases(TestCase):
expiry_date = self.now + timedelta(3) expiry_date = self.now + timedelta(3)
is_active = HepClient.is_product_active(expiry_date, TEACHER_ISBN) is_active = License.is_product_active(expiry_date, TEACHER_ISBN)
self.assertTrue(is_active) self.assertTrue(is_active)
def test_product_is_not_active(self): def test_product_is_not_active(self):
expiry_date = self.now - timedelta(3 * TEACHER_LICENSE['duration']) expiry_date = self.now - timedelta(3 * TEACHER_LICENSE['duration'])
is_active = HepClient.is_product_active(expiry_date, TEACHER_ISBN) is_active = License.is_product_active(expiry_date, TEACHER_ISBN)
self.assertFalse(is_active) self.assertFalse(is_active)
def test_token_is_not_valid_when_token_and_request_empty(self):
try:
self.hep_client._get_valid_token(None, None)
except HepClientNoTokenException:
return
self.fail("HepClientTestCases.test_token_is_not_valid_when_token_and_request_empty: Should throw HepClientUnauthorizedException")
@patch.object(OAuth2Token, 'is_dict_expired', return_value=True)
@patch.object(requests, 'post', return_value=MockResponse(400, data={}))
def test_token_is_expired_and_cannot_be_refreshed_from_api(self, mock_fn1, mock_fn2):
user = UserFactory(username='housi')
token = Oauth2TokenFactory(user=user).to_token()
try:
self.hep_client._get_valid_token(None, token)
except HepClientException:
return
self.fail("HepClientTestCases.test_token_is_expired_and_cannot_be_refreshed_from_api: Should throw HepClientUnauthorizedException")
@patch.object(OAuth2Token, 'is_dict_expired', return_value=True)
@patch.object(requests, 'post', return_value=MockResponse(200, data={}))
@patch.object(OAuth2Token, 'update_dict_with_refresh_data', return_value=(None, False))
def test_token_is_expired_and_cannot_be_refreshed(self, mock_fn1, mock_fn2, mock_fn3):
user = UserFactory(username='housi')
token = Oauth2TokenFactory(user=user).to_token()
try:
self.hep_client._get_valid_token(None, token)
except HepClientUnauthorizedException:
return
self.fail("HepClientTestCases.test_token_is_expired_and_cannot_be_refreshed: Should throw HepClientUnauthorizedException")
@patch.object(OAuth2Token, 'is_dict_expired', return_value=True)
@patch.object(requests, 'post', return_value=MockResponse(200, data=REFRESH_DATA))
def test_can_refresh_token(self, mock_fn1, mock_fn2):
user = UserFactory(username='housi')
token = Oauth2TokenFactory(user=user).to_token()
token_dict = self.hep_client._get_valid_token(None, token)
self.assertEqual(token_dict['access_token'], REFRESH_DATA['access_token'])
self.assertEqual(token_dict['refresh_token'], REFRESH_DATA['refresh_token'])

View File

@ -0,0 +1,220 @@
import time
from datetime import timedelta
from unittest.mock import patch
import requests
from authlib.integrations.django_client import DjangoRemoteApp
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from django.utils import timezone
from core.factories import UserFactory
from oauth.hep_client import HepClient
from oauth.user_signup_login_handler import EMAIL_NOT_VERIFIED, NO_VALID_LICENSE, UNKNOWN_ERROR
from oauth.views import authorize, OAUTH_REDIRECT
from users.tests.mock_hep_data_factory import MockResponse, ME_DATA, VALID_STUDENT_ORDERS, VALID_TEACHERS_ORDERS
from users.factories import LicenseFactory
from users.models import Role, User, SchoolClass, License, UserData
IN_A_HOUR = timezone.now() + timedelta(hours=1)
IN_A_HOUR_UNIX = time.mktime(IN_A_HOUR.timetuple())
TOKEN = {
'token_type': 'hep',
'access_token': '123456',
'refresh_token': 'abcdqwer',
'expires_at': IN_A_HOUR_UNIX,
}
NEW_ME_DATA = ME_DATA.copy()
NEW_ME_DATA['email'] = 'stiller@has.ch'
NEW_ME_DATA['id'] = 99
class LoginTests(TestCase):
def setUp(self):
self.user = UserFactory(username=ME_DATA['id'], email=ME_DATA['id'])
Role.objects.create_default_roles()
self.teacher_role = Role.objects.get_default_teacher_role()
self.factory = RequestFactory()
def _login(self, url):
request = self.factory.get(url)
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
request.user = AnonymousUser()
return authorize(request)
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=ME_DATA)
def test_user_data_is_synced_on_login(self, user_mock, authorize_mock):
old_mail = 'aschi@iterativ.ch'
self.user.hep_id = ME_DATA['id']
self.user.email = old_mail
self.user.username = old_mail
self.user.save()
now = timezone.now()
expiry_date = now + timedelta(365)
LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save()
response = self._login('/api/oauth/authorize?code=1234')
user = User.objects.get(hep_id=self.user.hep_id)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state=success')
self.assertEqual(user.username, ME_DATA['email'])
self.assertEqual(user.email, ME_DATA['email'])
self.assertTrue(self.user.is_authenticated)
@patch.object(HepClient, 'fetch_eorders', return_value=VALID_TEACHERS_ORDERS)
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=ME_DATA)
def test_teacher_can_login_with_valid_license(self, user_mock, authorize_mock, orders_mock):
response = self._login('/api/oauth/authorize?code=1234')
user = User.objects.get(email=ME_DATA['email'])
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY)
school_class = SchoolClass.objects.get(users__in=[user])
self.assertIsNotNone(school_class)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state=success')
self.assertTrue(self.user.is_authenticated)
try:
UserData.objects.get(user=user)
self.fail('LoginTests.test_teacher_can_login_with_valid_license: Userdata should not exist')
except:
pass
@patch.object(HepClient, 'fetch_eorders', return_value=VALID_STUDENT_ORDERS)
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=ME_DATA)
def test_student_can_login_with_valid_license(self, user_mock, authorize_mock, orders_mock):
response = self._login('/api/oauth/authorize?code=1234')
user = User.objects.get(email=ME_DATA['email'])
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.STUDENT_KEY)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.STUDENT_KEY)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state=success')
self.assertTrue(self.user.is_authenticated)
@patch.object(HepClient, 'is_email_verified', return_value=False)
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=ME_DATA)
def test_user_with_unconfirmed_email_cannot_login(self, user_mock, authorize_mock, verified_mock):
response = self._login('/api/oauth/authorize?code=1234')
User.objects.get(email=ME_DATA['email'])
self.assertEqual(302, response.status_code)
self.assertEqual(f'/{OAUTH_REDIRECT}?state={EMAIL_NOT_VERIFIED}', response.url)
@patch.object(HepClient, 'fetch_eorders', return_value=[])
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=ME_DATA)
def test_user_can_login_without_license(self, me_mock, product_mock, verified_mock):
response = self._login('/api/oauth/authorize?code=1234')
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state={NO_VALID_LICENSE}')
self.assertTrue(self.user.is_authenticated)
@patch.object(HepClient, 'fetch_eorders', return_value=[])
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=ME_DATA)
def test_user_can_login_local_license_invalid(self, me_mock, product_mock, verified_mock):
now = timezone.now()
expiry_date = now - timedelta(1)
LicenseFactory(expire_date=expiry_date, licensee=self.user, for_role=self.teacher_role).save()
response = self._login('/api/oauth/authorize?code=1234')
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state={NO_VALID_LICENSE}')
self.assertTrue(self.user.is_authenticated)
@patch.object(requests, 'get', return_value=MockResponse(500))
def test_user_gets_notified_if_server_error(self, post_mock):
response = self._login('/api/oauth/authorize?code=1234')
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state={UNKNOWN_ERROR}')
@patch.object(HepClient, 'fetch_eorders', return_value=VALID_TEACHERS_ORDERS)
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=NEW_ME_DATA)
def new_user_is_created_in_system_after_login_with_valid_license(self, user_mock, authorize_mock, orders_mock):
response = self._login('/api/oauth/authorize?code=1234')
try:
user = User.objects.get(email=NEW_ME_DATA['email'])
except:
self.fail('LoginTests.new_user_is_created_in_system_after_login: User was not created')
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY)
school_class = SchoolClass.objects.get(users__in=[user])
self.assertIsNotNone(school_class)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state=success')
self.assertTrue(self.user.is_authenticated)
try:
UserData.objects.get(user=user)
self.fail('LoginTests.test_teacher_can_login_with_valid_license: Userdata should not exist')
except:
pass
@patch.object(HepClient, 'fetch_eorders', return_value=[])
@patch.object(DjangoRemoteApp, 'authorize_access_token', return_value=TOKEN)
@patch.object(HepClient, 'user_details', return_value=NEW_ME_DATA)
def new_user_is_created_in_system_after_login(self, user_mock, authorize_mock, orders_mock):
response = self._login('/api/oauth/authorize?code=1234')
try:
user = User.objects.get(email=NEW_ME_DATA['email'])
except:
self.fail('LoginTests.new_user_is_created_in_system_after_login: User was not created')
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY)
school_class = SchoolClass.objects.get(users__in=[user])
self.assertIsNotNone(school_class)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/{OAUTH_REDIRECT}?state={NO_VALID_LICENSE}')
self.assertTrue(self.user.is_authenticated)
try:
UserData.objects.get(user=user)
self.fail('LoginTests.test_teacher_can_login_with_valid_license: Userdata should not exist')
except:
pass

View File

@ -1,5 +1,6 @@
from datetime import timedelta from datetime import timedelta
from django.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
@ -8,11 +9,19 @@ from core.utils import is_private_api_call_allowed
class MiddlewareTestCase(TestCase): class MiddlewareTestCase(TestCase):
def test_user_without_hep_id_cannot_see_private_api(self):
user = get_user_model().objects.create_user(username='sme')
body = b'"{mutation {\\n addRoom}"'
self.assertFalse(is_private_api_call_allowed(user, body))
def test_user_with_license_can_see_private_api(self): def test_user_with_license_can_see_private_api(self):
tomorrow = timezone.now() + timedelta(1) tomorrow = timezone.now() + timedelta(1)
user = UserFactory(username='aschiman@ch.ch') user = UserFactory(username='aschiman@ch.ch')
user.license_expiry_date = tomorrow user.license_expiry_date = tomorrow.date()
body = b'"{mutation {\\n addRoom}"' body = b'"{mutation {\\n addRoom}"'
@ -39,7 +48,7 @@ class MiddlewareTestCase(TestCase):
def test_logout_is_allowed_without_valid_license(self): def test_logout_is_allowed_without_valid_license(self):
yesterday = timezone.now() - timedelta(1) yesterday = timezone.now() - timedelta(1)
user = UserFactory(username='aschiman@ch.ch') user = UserFactory(username='aschiman@ch.ch', hep_id=34)
user.license_expiry_date = yesterday.date() user.license_expiry_date = yesterday.date()
body = b'"{mutation { logout {"' body = b'"{mutation { logout {"'
@ -49,7 +58,7 @@ class MiddlewareTestCase(TestCase):
def test_me_query_is_allowed_without_valid_license(self): def test_me_query_is_allowed_without_valid_license(self):
yesterday = timezone.now() - timedelta(1) yesterday = timezone.now() - timedelta(1)
user = UserFactory(username='aschiman@ch.ch') user = UserFactory(username='aschiman@ch.ch', hep_id=34)
user.license_expiry_date = yesterday user.license_expiry_date = yesterday
body = b'"{query { me {"' body = b'"{query { me {"'

View File

@ -0,0 +1,50 @@
from django.test import TestCase
from django.utils import timezone
from core.factories import UserFactory
from oauth.factories import Oauth2TokenFactory
REFRESH_DATA = {
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI5MzU3NDdmZC1mODM0LTQ0Y2MtODY0NC01YjA0ZGY2N2M4ZTMiLCJqdGkiOiI5ZmI5ZWFmOWM5Y2M4YWM4ZThlMDkyMDlkNWQwZjQxNTRiZTYwNmQ4ZDdkOTU0OTVkNTM0OWU3NWJmNDJiZGQ3MWRhYWU5MTJmMDIwNGMxNiIsImlhdCI6MTYyMzkyNjI1MC4xMzkxNDYsIm5iZiI6MTYyMzkyNjI1MC4xMzkxNTUsImV4cCI6MTY1NTQ2MjI1MC4xMDM2NTksInN1YiI6IjI5Iiwic2NvcGVzIjpbIm9yZGVycyJdLCJncm91cHMiOlsidGVzdHVzZXIiLCJ0ZWFjaGVyIl19.mdj0xP-GpPbwFt6VpnGq1RJND9SbfutcQkVv0I3G8HNEVylf17FuK22CMJRZLN2BW6hV67Kpps7RoCBPh9XWYUkkpLA1lD3RBZEit2IdBOhXf6G8B8go_UV2B8BUgHNn0AyLVWsawtdPkcIXbPkXv0oQAi-tsqFan5OE0XPQCUJfun2Cvhe4Teyl98-5zd_njt6mK_0BNtnDDAWjMTgVh9y_-WTu34S_2xttlh-vCFYMl8JwZPuNpTrCyD_UqfY8sp_dKPyg87BLRk4uR1iFoL399BvMSIUXoLFdh7Hb6eMuSBQH63JM77zuWk2bACBofULE2ajsbQg9a8dL43inNRwtRDlhofaw1NHYF_TrzRBP2pRgbo8FsEONVx9FRocMdfo4-icR1_Pb59Rr9lmiEu5JAi47o0rRCz9lAuUiHdliZtEPyAUQXJ5-y0zOITko83VstsU88OodgGvwZ53yp_aibdDBuX99YOSRvlBFXH0Sst49PEvGWnnNRP_4KOtAOzJ7n9yE0cDWo-VgB97KOVEv_BhiiE0SMbeYe7ByT8u9lNwKGX3AYWQTsbO5IlKn9f86NKBeLAB5bWaXXNnsQreNrTlhky8LoUQBtrSdwNWR7ZUheQOlSBKvqhT_48lJU_CMNxx38rmaoG6qC_WNKcFq_Lb01hLZ_VvYOPaIlWw",
"refresh_token": "def502007c4d524d828b97e4dc75198a482b3cf7db88dcef78b9c2bfe008ec8db48f02d564366b4f6d41115b090411bdc6c185fe06195aad2ef4e01cd6a7e4d87144eccf628c3864d669ec8fcebbaf2eb62fb42b367650e4dea8dbcc9465f5cf1ccb3e44ff3066f2ced4aac455677fd95f3abf36536f1828eb24c0c173858ca593a9d855ede03c7ec5a900930ca10b0a6270358ba114e3f695b5adfc7c51d0a67b4b29a617015213ae4193f28c9c2d8f8504a8573240085c948c6dc9654bc5f0d7447e22bf25278135005ba51f953a056cf8f64f55fe15447ba395160ce1e03f6861684763d0de11bc976d71f545548451539d2025b74693010661d1d3a7886644fc952f3adeb6f05350489f1cebdf947bca96112ccdb8ffd2b5b5d0a843ff2d3835d9cbee1f7442c8c5ba41d028f2e3b83523e510084c3c54f672820c92781aa63ce8a7cb9a938ba30562b8e69e23ab500a6f90eaa98a3e2ab40af88d20fc8a3c23185eab949d81ea9d737798aa022a03b8c1e5f987c77345cf5582debb726171484b1502871a8f95ce90e5d6"
}
class OAuth2TokenTestCases(TestCase):
def setUp(self):
user = UserFactory(username='housi')
self.token = Oauth2TokenFactory(user=user)
self.now = timezone.now()
def test_is_valid(self):
self.assertFalse(self.token.has_expired())
def test_has_expired(self):
one_hourish_delta_in_ms = 60*60
self.token.expires_at -= one_hourish_delta_in_ms
self.token.save()
self.assertTrue(self.token.has_expired())
def test_can_update_refresh_data(self):
token, success = self.token.update_with_refresh_data(REFRESH_DATA)
self.assertTrue(success)
self.assertEqual(REFRESH_DATA['access_token'], token.access_token)
def test_success_on_update_refresh_data(self):
token, success = self.token.update_with_refresh_data(REFRESH_DATA)
self.assertTrue(success)
self.assertEqual(REFRESH_DATA['access_token'], token.access_token)
def test_fail_on_update_refresh_data(self):
data = REFRESH_DATA.copy()
data['access_token'] = '12344'
token, success = self.token.update_with_refresh_data(data)
self.assertFalse(success)
self.assertIsNone(token)

8
server/oauth/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.conf.urls import url
from oauth import views
app_name = 'users'
urlpatterns = [
url(r'^login/', views.login, name='login'),
url(r'^callback/', views.authorize, name='authorize')
]

View File

@ -1,5 +1,4 @@
from core.hep_client import HepClient, HepClientException from oauth.hep_client import HepClient, HepClientException
from core.models import AdminData
from users.models import License from users.models import License
from users.models import User, UserRole, Role, SchoolClass from users.models import User, UserRole, Role, SchoolClass
@ -9,13 +8,10 @@ UNKNOWN_ERROR = 'unknown_error'
NO_VALID_LICENSE = 'no_valid_license' NO_VALID_LICENSE = 'no_valid_license'
def handle_user_and_verify_products(user_data): def handle_user_and_verify_products(user_data, token):
hep_client = HepClient() hep_client = HepClient()
try: user = User.objects.get_or_create_hep_user(user_data)
user = User.objects.get(hep_id=user_data['id'])
except User.DoesNotExist:
user = User.objects.create_user_from_hep(user_data)
try: try:
if not hep_client.is_email_verified(user_data): if not hep_client.is_email_verified(user_data):
@ -26,7 +22,7 @@ def handle_user_and_verify_products(user_data):
license = License.objects.get_active_license_for_user(user) license = License.objects.get_active_license_for_user(user)
if not license: if not license:
license, error_msg = check_and_create_licenses(hep_client, user) license, error_msg = check_and_create_licenses(hep_client, user, token)
if error_msg: if error_msg:
return user, error_msg return user, error_msg
@ -39,10 +35,9 @@ def handle_user_and_verify_products(user_data):
return user, None return user, None
def check_and_create_licenses(hep_client, user): def check_and_create_licenses(hep_client, user, token):
try: try:
admin_token = AdminData.objects.get_admin_token() product = hep_client.active_myskillbox_product_for_customer(token_dict=token)
product = hep_client.myskillbox_product_for_customer(admin_token, user.hep_id)
except HepClientException: except HepClientException:
return None, UNKNOWN_ERROR return None, UNKNOWN_ERROR
@ -50,7 +45,6 @@ def check_and_create_licenses(hep_client, user):
license = License.objects.create_license_for_role(user, product['activated'], product['raw'], license = License.objects.create_license_for_role(user, product['activated'], product['raw'],
product['license']['edition'], product['license']['edition'],
product['order_id'], product['isbn']) product['order_id'], product['isbn'])
# todo handle no license case
else: else:
return None, NO_VALID_LICENSE return None, NO_VALID_LICENSE

48
server/oauth/views.py Normal file
View File

@ -0,0 +1,48 @@
from authlib.integrations.base_client import OAuthError
from django.conf import settings
from django.shortcuts import redirect
from sentry_sdk import capture_exception
from oauth.hep_client import HepClient
from oauth.oauth_client import oauth
from oauth.models import OAuth2Token
from oauth.user_signup_login_handler import handle_user_and_verify_products, EMAIL_NOT_VERIFIED, UNKNOWN_ERROR
from django.contrib.auth import login as dj_login
from core.logger import get_logger
logger = get_logger(__name__)
OAUTH_REDIRECT = 'oauth-redirect'
def login(request):
hep_oauth_client = oauth.create_client('hep')
redirect_uri = settings.OAUTH_LOCAL_REDIRECT_URI
return hep_oauth_client.authorize_redirect(request, redirect_uri)
def authorize(request):
hep_client = HepClient()
try:
token = oauth.hep.authorize_access_token(request)
user_data = hep_client.user_details(token_dict=token)
user, status_msg = handle_user_and_verify_products(user_data, token)
user.sync_with_hep_data(user_data)
except OAuthError as e:
logger.warning(f'OAuth error: {e}')
if not settings.DEBUG:
capture_exception(e)
return redirect(f'/{OAUTH_REDIRECT}?state={UNKNOWN_ERROR}')
if user and status_msg != EMAIL_NOT_VERIFIED:
dj_login(request, user)
OAuth2Token.objects.update_or_create_token(token, user)
if status_msg:
return redirect(f'/{OAUTH_REDIRECT}?state={status_msg}')
return redirect(f'/{OAUTH_REDIRECT}?state=success')

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>

View File

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
from django.apps import AppConfig
class UserConfig(AppConfig):
name = 'registration'

View File

@ -1,45 +0,0 @@
# Generated by Django 2.0.6 on 2019-10-09 09:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0009_auto_20191009_0905'),
]
operations = [
migrations.CreateModel(
name='License',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='LicenseType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='License name')),
('key', models.CharField(max_length=128)),
('active', models.BooleanField(default=False, verbose_name='License active')),
('description', models.TextField(default='', verbose_name='Description')),
('for_role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Role')),
],
),
migrations.AddField(
model_name='license',
name='license_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='registration.LicenseType'),
),
migrations.AddField(
model_name='license',
name='licensee',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.0.6 on 2019-10-10 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='licensetype',
name='key',
field=models.CharField(max_length=128, unique=True),
),
]

View File

@ -1,41 +0,0 @@
# Generated by Django 2.0.6 on 2020-02-04 13:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0010_schoolclass_code'),
('registration', '0002_auto_20191010_0905'),
]
operations = [
migrations.RemoveField(
model_name='licensetype',
name='for_role',
),
migrations.RemoveField(
model_name='license',
name='license_type',
),
migrations.AddField(
model_name='license',
name='expire_date',
field=models.DateField(null=True),
),
migrations.AddField(
model_name='license',
name='for_role',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.Role'),
),
migrations.AddField(
model_name='license',
name='raw',
field=models.TextField(default=''),
),
migrations.DeleteModel(
name='LicenseType',
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 2.1.15 on 2020-02-20 10:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('registration', '0003_auto_20200204_1331'),
]
operations = [
migrations.RemoveField(
model_name='license',
name='for_role',
),
migrations.RemoveField(
model_name='license',
name='licensee',
),
migrations.DeleteModel(
name='License',
),
]

View File

@ -1,68 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
import graphene
from django.contrib.auth import login
from graphene import relay
from core.hep_client import HepClient, HepClientException
from core.models import AdminData
from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR, NO_VALID_LICENSE
class Registration(relay.ClientIDMutation):
class Input:
confirmation_key = graphene.String()
user_id = graphene.Int()
success = graphene.Boolean()
message = graphene.String()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
confirmation_key = kwargs.get('confirmation_key')
user_id = kwargs.get('user_id')
hep_client = HepClient()
admin_token = AdminData.objects.get_admin_token()
try:
hep_client.customer_activate(confirmation_key, user_id)
user_data = hep_client.customers_by_id(admin_token, user_id)
# double check if user has verified his email. If the "confirmation" field is present, the email address
# is not verified.
if 'confirmation' in user_data:
return cls.return_fail_registration_msg('invalid_key')
except HepClientException:
return cls.return_fail_registration_msg('unknown_error')
user, status_msg = handle_user_and_verify_products(user_data)
if user:
login(info.context, user)
if status_msg:
if status_msg == NO_VALID_LICENSE:
return cls(success=True, message=NO_VALID_LICENSE)
else:
return cls.return_fail_registration_msg(status_msg)
return cls(success=True, message='success')
@classmethod
def return_fail_registration_msg(cls, message):
if message == UNKNOWN_ERROR:
raise Exception(message)
return cls(success=False, message=message)
class RegistrationMutations:
registration = Registration.Field()

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings

View File

@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
from unittest.mock import patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema_public import schema
from core.hep_client import HepClient
from core.tests.mock_hep_data_factory import ME_DATA, VALID_TEACHERS_ORDERS
from users.models import License
from users.models import User, Role, SchoolClass
INVALID_KEY_ME = dict(ME_DATA)
INVALID_KEY_ME['confirmation'] = 'abddddddd'
class RegistrationTests(TestCase):
def setUp(self):
request = RequestFactory().post('/')
Role.objects.create_default_roles()
self.email = 'sepp@skillbox.iterativ.ch'
self.first_name = 'Sepp'
self.last_name = 'Feuz'
# adding session
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
self.client = Client(schema=schema, context_value=request)
def make_register_mutation(self, confirmation_key, user_id):
mutation = '''
mutation Registration($input: RegistrationInput!){
registration(input: $input) {
success
message
}
}
'''
return self.client.execute(mutation, variables={
'input': {
'confirmationKey': confirmation_key,
'userId': user_id
}
})
@patch.object(HepClient, 'customer_activate', return_value="Response")
@patch.object(HepClient, 'customers_by_id', return_value=ME_DATA)
@patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None)
@patch.object(HepClient, 'fetch_admin_token', return_value=b'"AABBCCDDEE**44566"')
def test_user_can_register_with_valid_confirmation_key_and_no_license(self, admin_mock, customer_by_id_mock,
product_mock, customer_mock):
result = self.make_register_mutation('CONFIRMATION_KEY', 1)
self.assertTrue(result.get('data').get('registration').get('success'))
self.assertEqual(result.get('data').get('registration').get('message'), 'no_valid_license')
@patch.object(HepClient, 'customer_activate', return_value="Response")
@patch.object(HepClient, 'customers_by_id', return_value=INVALID_KEY_ME)
@patch.object(HepClient, 'fetch_admin_token', return_value=b'"AABBCCDDEE**44566"')
def test_user_cannot_register_with_invalid_key(self, admin_mock, confirmation_mock, id_mock):
result = self.make_register_mutation('CONFIRMATION_KEY', 1)
self.assertFalse(result.get('data').get('registration').get('success'))
self.assertEqual(result.get('data').get('registration').get('message'), 'invalid_key')
@patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS)
@patch.object(HepClient, 'customer_activate', return_value="Response")
@patch.object(HepClient, 'customers_by_id', return_value=ME_DATA)
@patch.object(HepClient, 'fetch_admin_token', return_value=b'"AABBCCDDEE**44566"')
def test_teacher_can_register_with_remote_license(self, admin_mock, id_mock, activate_mock, orders_mock):
result = self.make_register_mutation('CONFIRMATION_KEY', 1)
user = User.objects.get(email=ME_DATA['email'])
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY)
school_class = SchoolClass.objects.get(users__in=[user])
self.assertIsNotNone(school_class)
self.assertTrue(result.get('data').get('registration').get('success'))
self.assertTrue(user.is_authenticated)

View File

@ -1,95 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 25.02.20
# @author: chrigu <christian.cueni@iterativ.ch>
import json
from unittest.mock import patch
import requests
from django.test import TestCase, Client
from django.urls import reverse
from core.hep_client import HepClient
from core.tests.mock_hep_data_factory import MockResponse
RESPONSE = {
'id': 1234,
'confirmation': 'abdc1234',
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'email': 'aschima@ch.ch',
'prefix': 'Herr',
'gender': 1,
'addresses': [
{
'country_id': 'CH',
'street': ['Weg 1'],
'postcode': '1234',
'city': 'Äussere Einöde',
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'prefix': 'Herr',
'default_shipping': True,
'default_billing': True,
}
],
}
DATA = {
'accepted_terms': True,
'customer': {
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'email': 'aschima@ch.ch',
'prefix': 'Herr',
'gender': 1,
'addresses': [
{
'country_id': 'CH',
'street': ['Weg 1'],
'postcode': '1234',
'city': 'Äussere Einöde',
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'prefix': 'Herr',
'default_shipping': True,
'default_billing': True,
}
],
'password': '123454abasfd'
}
}
class ProxyTest(TestCase):
def setUp(self):
self.client = Client()
@patch.object(HepClient, 'customer_create', return_value=RESPONSE)
def test_proxy_filters_confirmation_key(self, create_mock):
response = self.client.post(reverse('api:registration:proxy'), json.dumps(DATA), content_type="application/json")
found = 'confirmation' in response.json().keys()
self.assertFalse(found)
@patch.object(requests, 'post', return_value=MockResponse(400,
data={'message': 'Ein Kunde mit der gleichen E-Mail-Adresse existiert bereits in einer zugeordneten Website.'}))
def test_handles_400(self, create_mock):
response = self.client.post(reverse('api:registration:proxy'), json.dumps(DATA), content_type="application/json")
self.assertEquals(response.json()['message'], 'Ein Kunde mit der gleichen E-Mail-Adresse existiert bereits in einer zugeordneten Website.')
def test_requires_accepted_terms(self):
del DATA['accepted_terms']
response = self.client.post(reverse('api:registration:proxy'), json.dumps(DATA), content_type="application/json")
self.assertEquals(response.status_code, 400)
self.assertEquals(response.json()['message'], 'Sie müssen hier zustimmen, damit Sie sich registrieren können.')

View File

@ -1,9 +0,0 @@
from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt
from registration.view import RegistrationProxyView
app_name = 'registration'
urlpatterns = [
url(r'^registration/', csrf_exempt(RegistrationProxyView.as_view()), name="proxy"),
]

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 25.02.20
# @author: chrigu <christian.cueni@iterativ.ch>
import json
from django.http import JsonResponse
from django.views import View
from core.hep_client import HepClient, HepClientException
class RegistrationProxyView(View):
def post(self, request, *args, **kwargs):
hep_client = HepClient()
data = json.loads(request.body)
if not self.terms_accepted(data):
return JsonResponse(
{
'message': 'Sie müssen hier zustimmen, damit Sie sich registrieren können.'
},
status=400)
data['customer']['group_id'] = 5
try:
hep_data = hep_client.customer_create(data)
except HepClientException as e:
return JsonResponse(e.args[1], status=e.args[0])
response_data = hep_data.copy()
del response_data['confirmation']
return JsonResponse(response_data)
def terms_accepted(self, data):
if 'accepted_terms' in data and data['accepted_terms']:
del data['accepted_terms']
return True
return False

View File

@ -1,8 +1,11 @@
import random import random
from datetime import timedelta
import factory import factory
from users.models import SchoolClass, SchoolClassMember, License, Team from users.models import SchoolClass, SchoolClassMember, License, Team
from django.utils.timezone import now
class_types = ['DA', 'KV', 'INF', 'EE'] class_types = ['DA', 'KV', 'INF', 'EE']
class_suffix = ['A', 'B', 'C', 'D', 'E'] class_suffix = ['A', 'B', 'C', 'D', 'E']
@ -44,3 +47,6 @@ class TeamFactory(factory.django.DjangoModelFactory):
class LicenseFactory(factory.django.DjangoModelFactory): class LicenseFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = License model = License
expire_date = now() + timedelta(days=7)
order_id = factory.Sequence(lambda n: n)

35
server/users/licenses.py Normal file
View File

@ -0,0 +1,35 @@
TEACHER_KEY = 'teacher'
STUDENT_KEY = 'student'
MYSKILLBOX_LICENSES = {
"978-3-0355-1397-4": {
'edition': STUDENT_KEY,
'duration': 4 * 365,
'name': 'Student 4 years'
},
"978-3-0355-1860-3": {
'edition': STUDENT_KEY,
'duration': 455,
'name': 'Student 1 year'
},
"978-3-0355-1862-7": {
'edition': STUDENT_KEY,
'duration': 30,
'name': 'Student test 1 month'
},
"978-3-0355-1861-0": {
'edition': TEACHER_KEY,
'duration': 30,
'name': 'Teacher test 1 month'
},
"978-3-0355-1823-8": {
'edition': TEACHER_KEY,
'duration': 455,
'name': 'Teacher 1 year'
}
}
def is_myskillbox_product(isbn):
valid_isbns = list(MYSKILLBOX_LICENSES.keys())
return isbn in valid_isbns

View File

@ -1 +0,0 @@
from django.conf import settings

View File

@ -1,32 +0,0 @@
import os
import shutil
from django.conf import settings
from django.core.management import BaseCommand
from core.hep_client import HepClient
from core.models import AdminData
from users.models import User, License
class Command(BaseCommand):
def handle(self, *args, **options):
"Update licenses via cronjob"
hep_client = HepClient()
admin_token = AdminData.objects.get_admin_token()
hep_users = User.objects.filter(hep_id__isnull=False)
for hep_user in hep_users:
product = hep_client.myskillbox_product_for_customer(admin_token, hep_user.hep_id)
if product and License.objects.filter(licensee=hep_user, order_id=product['order_id']).count() == 0:
license = License.objects.create_license_for_role(hep_user, product['activated'], product['raw'],
product['license']['edition'], product['order_id'],
product['isbn'])
if license.is_valid():
hep_user.license_expiry_date = license.expire_date
hep_user.save()

View File

@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from django.db import models from django.db import models
from django.contrib.auth.models import UserManager as DjangoUserManager from django.contrib.auth.models import UserManager as DjangoUserManager
from core.hep_client import MYSKILLBOX_LICENSES from users.licenses import MYSKILLBOX_LICENSES
class RoleManager(models.Manager): class RoleManager(models.Manager):
@ -113,16 +113,25 @@ class UserManager(DjangoUserManager):
user = self.model.objects.get(email=user_data['email']) user = self.model.objects.get(email=user_data['email'])
user.set_unusable_password() user.set_unusable_password()
except self.model.DoesNotExist: except self.model.DoesNotExist:
user = self._create_user_with_random_password_no_save( user_data['firstname'], user = self._create_user_with_random_password_no_save(user_data['first_name'],
user_data['lastname'], user_data['last_name'],
user_data['email']) user_data['email'])
user.hep_id = user_data['id'] user.hep_id = user_data['id']
user.hep_group_id = user_data['group_id']
user.save() user.save()
if user.hep_group_id == settings.HEP_MYSKILLBOX_GROUP_ID: # todo: how to handle
apps.get_model('users.UserData').objects.create(user=user, accepted_terms=True) # if user.hep_group_id == settings.HEP_MYSKILLBOX_GROUP_ID:
# apps.get_model('users.UserData').objects.create(user=user, accepted_terms=True)
return user
def get_or_create_hep_user(self, user_data):
try:
user = self.get(hep_id=user_data['id'])
except self.model.DoesNotExist:
user = self.create_user_from_hep(user_data)
return user return user

View File

@ -1,15 +1,17 @@
import random import random
import re import re
from datetime import datetime, timedelta, date
import string import string
from datetime import date, datetime
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, Permission from django.contrib.auth.models import AbstractUser, Permission
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.utils.timezone import make_aware, is_aware
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from core.hep_client import HepClient, MYSKILLBOX_LICENSES from users.licenses import MYSKILLBOX_LICENSES
from users.managers import RoleManager, UserRoleManager, UserManager, LicenseManager from users.managers import RoleManager, UserRoleManager, UserManager, LicenseManager
DEFAULT_SCHOOL_ID = 1 DEFAULT_SCHOOL_ID = 1
@ -94,12 +96,12 @@ class User(AbstractUser):
self.username = hep_data['email'] self.username = hep_data['email']
data_has_changed = True data_has_changed = True
if self.first_name != hep_data['firstname']: if self.first_name != hep_data['first_name']:
self.first_name = hep_data['firstname'] self.first_name = hep_data['first_name']
data_has_changed = True data_has_changed = True
if self.last_name != hep_data['lastname']: if self.last_name != hep_data['last_name']:
self.last_name = hep_data['lastname'] self.last_name = hep_data['last_name']
data_has_changed = True data_has_changed = True
if data_has_changed: if data_has_changed:
@ -304,8 +306,17 @@ class License(models.Model):
return self.for_role.key == RoleManager.TEACHER_KEY return self.for_role.key == RoleManager.TEACHER_KEY
def is_valid(self): def is_valid(self):
return HepClient.is_product_active( date = make_aware(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day))
datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day), self.isbn) return License.is_product_active(date, self.isbn)
@staticmethod
def is_product_active(expiry_date, isbn):
now = timezone.now()
if not is_aware(expiry_date):
expiry_date = make_aware(expiry_date)
return expiry_date >= now >= expiry_date - timedelta(days=MYSKILLBOX_LICENSES[isbn]['duration'])
def __str__(self): def __str__(self):
return f'License for role: {self.for_role}' return f'License for role: {self.for_role}'

View File

@ -3,8 +3,7 @@ from django.conf import settings
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from graphene import relay from graphene import relay
from core.hep_client import HepClient, HepClientUnauthorizedException, HepClientException # from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR, EMAIL_NOT_VERIFIED
from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR, EMAIL_NOT_VERIFIED
class BetaLogin(relay.ClientIDMutation): class BetaLogin(relay.ClientIDMutation):
@ -30,46 +29,7 @@ class BetaLogin(relay.ClientIDMutation):
raise Exception('not_implemented') raise Exception('not_implemented')
class Login(relay.ClientIDMutation):
class Input:
token_input = graphene.String()
success = graphene.Boolean()
message = graphene.String()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
hep_client = HepClient()
token = kwargs.get('token_input')
try:
user_data = hep_client.customer_me(token)
except HepClientUnauthorizedException:
return cls.return_login_message('invalid_credentials')
except HepClientException:
return cls.return_login_message(UNKNOWN_ERROR)
user, status_msg = handle_user_and_verify_products(user_data)
user.sync_with_hep_data(user_data)
if user and status_msg != EMAIL_NOT_VERIFIED:
login(info.context, user)
if status_msg:
return cls.return_login_message(status_msg)
return cls(success=True, message='success')
@classmethod
def return_login_message(cls, message):
if message == EMAIL_NOT_VERIFIED or message == UNKNOWN_ERROR or message == 'invalid_credentials':
raise Exception(message)
return cls(success=True, message=message)
class UserMutations: class UserMutations:
login = Login.Field()
beta_login = BetaLogin.Field() beta_login = BetaLogin.Field()

View File

@ -1,5 +1,10 @@
from datetime import timedelta
from django.utils.timezone import now
from core.factories import UserFactory from core.factories import UserFactory
from users.factories import SchoolClassFactory from users.factories import SchoolClassFactory, LicenseFactory
from users.licenses import MYSKILLBOX_LICENSES
from users.models import Role, UserRole, DEFAULT_SCHOOL_ID from users.models import Role, UserRole, DEFAULT_SCHOOL_ID
@ -47,6 +52,9 @@ def create_users(data=None):
) )
else: else:
in_a_week = now() + timedelta(days=7)
hep_id = 1
for school_class in data: for school_class in data:
first, last = school_class.get('teacher') first, last = school_class.get('teacher')
teacher = UserFactory( teacher = UserFactory(
@ -54,19 +62,26 @@ def create_users(data=None):
first_name=first, first_name=first,
last_name=last, last_name=last,
email='{}.{}@skillbox.example'.format(first, last).lower(), email='{}.{}@skillbox.example'.format(first, last).lower(),
onboarding_visited=True onboarding_visited=True,
license_expiry_date=in_a_week,
hep_id=hep_id
) )
UserRole.objects.create(user=teacher, role=teacher_role) UserRole.objects.create(user=teacher, role=teacher_role)
students = [] students = []
for first, last in school_class.get('students'): for first, last in school_class.get('students'):
hep_id += 1
student = create_student( student = create_student(
username='{}.{}'.format(first, last).lower(), username='{}.{}'.format(first, last).lower(),
first_name=first, first_name=first,
last_name=last, last_name=last,
email='{}.{}@skillbox.example'.format(first, last).lower(), email='{}.{}@skillbox.example'.format(first, last).lower(),
onboarding_visited=True onboarding_visited=True,
license_expiry_date=in_a_week,
hep_id=hep_id
) )
students.append(student) students.append(student)
SchoolClassFactory( SchoolClassFactory(

View File

@ -0,0 +1,12 @@
{
"id": 5,
"email": "hans.meier@iterativ.ch",
"email_verified_at": 123456789,
"salutation": null,
"company": null,
"first_name": "Hans",
"last_name": "Meier",
"phone": null,
"institution": null,
"name": "Hans Meier"
}

Some files were not shown because too many files have changed in this diff Show More