commit
0ff9404cbe
|
|
@ -43,5 +43,3 @@ server/media/
|
|||
|
||||
.coverage
|
||||
|
||||
# Cypress screenshots
|
||||
client/cypress/screenshots
|
||||
|
|
|
|||
3
Pipfile
3
Pipfile
|
|
@ -35,7 +35,8 @@ django-libsass = "*"
|
|||
bleach = "*"
|
||||
newrelic = "*"
|
||||
sentry-sdk = "==0.7.2"
|
||||
"django-sendgrid-v5" = "*"
|
||||
"django-sendgrid-v5" = "==0.8.0"
|
||||
python-http-client = "==3.2.1"
|
||||
coverage = "*"
|
||||
graphql-relay = "*"
|
||||
ipython = "*"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "85020e50720b4f71b864719a7c4fae8a0cb4e80352d3bc7eb1e4726bf2405f6d"
|
||||
"sha256": "725dd26226fe58559f67b30a09dec72086dc4681a69f2f2672f5bd87c9ac74b8"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
|
@ -23,6 +23,14 @@
|
|||
],
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"appnope": {
|
||||
"hashes": [
|
||||
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
|
||||
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
|
||||
],
|
||||
"markers": "sys_platform == 'darwin'",
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||
|
|
@ -48,18 +56,18 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:778e66711d7de9352b6330692ae44c0ba012559dabb4c39d7416b0135fb335a6",
|
||||
"sha256:c8371f1f9c52f64cef1f14a773629fc8434ecca6195b2ef2a429a7bbbf8ecf23"
|
||||
"sha256:08d949fede71c14db8b9b638edaa13831d79daed84e2da27750629fd606bdb57",
|
||||
"sha256:4d7c2cc266917cd0ff7e5e039158de80991e21696a2e8bf85201de2d06d7ceea"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.9.228"
|
||||
"version": "==1.10.9"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:7c391c46cf1f8d6c04758f84b51337a64136077e4a160eced87551fdcc051669",
|
||||
"sha256:c9148df92ba21a90ea32f2c7f185c31b1c8b8e48417d0ba8cad02b9b3336c09a"
|
||||
"sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1",
|
||||
"sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3"
|
||||
],
|
||||
"version": "==1.12.228"
|
||||
"version": "==1.13.9"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
|
|
@ -122,10 +130,10 @@
|
|||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
|
||||
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
|
||||
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
|
||||
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
"version": "==4.4.1"
|
||||
},
|
||||
"dj-database-url": {
|
||||
"hashes": [
|
||||
|
|
@ -242,9 +250,9 @@
|
|||
},
|
||||
"draftjs-exporter": {
|
||||
"hashes": [
|
||||
"sha256:503f222c81de9a0619158d8f88b638f9069af8de233dc020faa782c7a3b22100"
|
||||
"sha256:5839cbc29d7bce2fb99837a404ca40c3a07313f2a20e2700de7ad6aa9a9a18fb"
|
||||
],
|
||||
"version": "==2.1.6"
|
||||
"version": "==2.1.7"
|
||||
},
|
||||
"factory-boy": {
|
||||
"hashes": [
|
||||
|
|
@ -256,16 +264,16 @@
|
|||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:1d3f700e8dfcefd6e657118d71405d53e86974448aba78884f119bbd84c0cddf",
|
||||
"sha256:d5366e120191c5610fceeebfe1c298dc46da0277096f639c6dd7e2eaee0fa547"
|
||||
"sha256:5902379d8df308a204fc11c4f621590ee83975805a6c7b2228203b9defa45250",
|
||||
"sha256:5e8c755c619f332d5ec28b7586389665f136bcf528e165eb925e87c06a63eda7"
|
||||
],
|
||||
"version": "==2.0.1"
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"version": "==0.17.1"
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"graphene": {
|
||||
"hashes": [
|
||||
|
|
@ -322,11 +330,11 @@
|
|||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b",
|
||||
"sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"
|
||||
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
|
||||
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.8.0"
|
||||
"version": "==7.9.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
|
|
@ -351,30 +359,31 @@
|
|||
},
|
||||
"libsass": {
|
||||
"hashes": [
|
||||
"sha256:2457723fe04f4e690105f758aa125e809afc840812965095fa3f4edccd6275ef",
|
||||
"sha256:2974772e7984b27a51a6d91ebc140183ddd574a9663bd02154ddfb75f13a3eed",
|
||||
"sha256:2d067ce4f393fee2ce52bb810a364deac5454dfdb7945d31d1f4265f21f03ab8",
|
||||
"sha256:57d0b99c4e3512233a44141f1bf852570d359724a606dfc4550eccd0f570460d",
|
||||
"sha256:5b604e4f5befdecc76240c2ba243fd7e23c642ffc2dd86cbfd094a44ead6b08d",
|
||||
"sha256:5dd647ffa1319a2a18572f41fee3bb561d7f77d8d4784074a00b2eb22c61a859",
|
||||
"sha256:78f3f14e47612be4fa4b161278f2a3e880a19b6a3367f749e9ae240434b7e7f5",
|
||||
"sha256:8d423e4b4c0e219488104b4ec4267688dbd816f3ae806beb4201918eff059b2d",
|
||||
"sha256:a20473b0427d82e37fa68f0b3a8d219f0bb5ca6d3f7d93b0f5342219285e7064",
|
||||
"sha256:c1f76c2a0993914f3c3088e9b6c7031f22e879c5d27a060cdc8c5aa1318eb9b6",
|
||||
"sha256:c99fbc950f1955e8b6370aafdb9d84d324e4984a2e00a2b47f04dbcc3706a9d1",
|
||||
"sha256:cb50f385117535f7671ac7ff3144c1ef0b8e088778c58d269ce6f31b87bfad72",
|
||||
"sha256:f0f033a8154be60e1a2e1f79ee849ea69a1d62e5d476a78f69e4c7d8fd7c20e1",
|
||||
"sha256:f2572b73b2e13e74b28388ae86c4fabb853ddbfc12279b4444243bd614710ce8",
|
||||
"sha256:f8790db67e00c5bc7be1bdd81ed477563a4b191e839193ecc0c2c5ec679ec481"
|
||||
"sha256:003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2",
|
||||
"sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272",
|
||||
"sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08",
|
||||
"sha256:4dcfd561fb100250b89496e1362b96f2cc804f689a59731eb0f94f9a9e144f4a",
|
||||
"sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f",
|
||||
"sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0",
|
||||
"sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32",
|
||||
"sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c",
|
||||
"sha256:845a9573b25c141164972d498855f4ad29367c09e6d76fad12955ad0e1c83013",
|
||||
"sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95",
|
||||
"sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c",
|
||||
"sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1",
|
||||
"sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404",
|
||||
"sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e",
|
||||
"sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a",
|
||||
"sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"
|
||||
],
|
||||
"version": "==0.19.2"
|
||||
"version": "==0.19.4"
|
||||
},
|
||||
"newrelic": {
|
||||
"hashes": [
|
||||
"sha256:9d6d3bf2e125410d485fd91279e1b0f50e8f0f5887284b345aed4ec81db33c0a"
|
||||
"sha256:da9adab674d9fe7aa8fbabb6691b33fb7be94a32b6a80548ec7018be9df8ef65"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.2.126"
|
||||
"version": "==5.2.1.129"
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
|
|
@ -438,11 +447,11 @@
|
|||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
||||
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
||||
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
||||
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
|
||||
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
|
||||
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
|
||||
],
|
||||
"version": "==2.0.9"
|
||||
"version": "==2.0.10"
|
||||
},
|
||||
"psycopg2": {
|
||||
"hashes": [
|
||||
|
|
@ -517,14 +526,15 @@
|
|||
"sha256:c2776054245db376ea26c859b80e9280b1a470b96ed998d60d35951f89bbbe79",
|
||||
"sha256:e455ae0dfd5819ac483f7fecf08ab8693048d9dc47a0a6fe0d4aebf86d9d1d17"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.1"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
],
|
||||
"version": "==2019.2"
|
||||
"version": "==2019.3"
|
||||
},
|
||||
"raven": {
|
||||
"hashes": [
|
||||
|
|
@ -611,17 +621,17 @@
|
|||
},
|
||||
"text-unidecode": {
|
||||
"hashes": [
|
||||
"sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d",
|
||||
"sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"
|
||||
"sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
|
||||
"sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
|
||||
],
|
||||
"version": "==1.2"
|
||||
"version": "==1.3"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
||||
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
||||
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
|
||||
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
|
||||
],
|
||||
"version": "==4.3.2"
|
||||
"version": "==4.3.3"
|
||||
},
|
||||
"typing": {
|
||||
"hashes": [
|
||||
|
|
@ -640,11 +650,11 @@
|
|||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==1.25.3"
|
||||
"version": "==1.25.6"
|
||||
},
|
||||
"wagtail": {
|
||||
"hashes": [
|
||||
|
|
@ -689,13 +699,21 @@
|
|||
}
|
||||
},
|
||||
"develop": {
|
||||
"appnope": {
|
||||
"hashes": [
|
||||
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
|
||||
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
|
||||
],
|
||||
"markers": "sys_platform == 'darwin'",
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"awscli": {
|
||||
"hashes": [
|
||||
"sha256:5c36a818130c8393b778e5b98b9bf38985522b62a684782512a2d57340697582",
|
||||
"sha256:a022dfb7e6cc9ee8540c382b9bdc4637070dbbe2b09f3a74ad4f30719414c0b7"
|
||||
"sha256:4a75cb44dc3dab14bc9bdb0d2731c37a5026bf0afa4acb620fec72c74e62915a",
|
||||
"sha256:90b0b3e91a900e4569bc47f29769522337c46ff50e35f9e4a41830fdb425f000"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.16.238"
|
||||
"version": "==1.16.273"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
|
|
@ -706,17 +724,18 @@
|
|||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:7c391c46cf1f8d6c04758f84b51337a64136077e4a160eced87551fdcc051669",
|
||||
"sha256:c9148df92ba21a90ea32f2c7f185c31b1c8b8e48417d0ba8cad02b9b3336c09a"
|
||||
"sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1",
|
||||
"sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3"
|
||||
],
|
||||
"version": "==1.12.228"
|
||||
"version": "==1.13.9"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
],
|
||||
"version": "==0.3.9"
|
||||
"markers": "python_version != '2.6' and python_version != '3.3'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
|
|
@ -758,10 +777,10 @@
|
|||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
|
||||
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
|
||||
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
|
||||
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
|
||||
],
|
||||
"version": "==4.4.0"
|
||||
"version": "==4.4.1"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
|
|
@ -780,11 +799,11 @@
|
|||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b",
|
||||
"sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"
|
||||
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
|
||||
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.8.0"
|
||||
"version": "==7.9.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
|
|
@ -831,11 +850,11 @@
|
|||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
||||
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
||||
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
||||
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
|
||||
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
|
||||
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
|
||||
],
|
||||
"version": "==2.0.9"
|
||||
"version": "==2.0.10"
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
|
|
@ -882,7 +901,7 @@
|
|||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
||||
],
|
||||
"markers": "python_version != '2.6'",
|
||||
"markers": "python_version != '2.6' and python_version != '3.3'",
|
||||
"version": "==5.1.2"
|
||||
},
|
||||
"rsa": {
|
||||
|
|
@ -908,18 +927,18 @@
|
|||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
||||
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
||||
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
|
||||
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
|
||||
],
|
||||
"version": "==4.3.2"
|
||||
"version": "==4.3.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==1.25.3"
|
||||
"version": "==1.25.6"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -35,7 +35,7 @@ docker run --name skillboxdb -d -p 5432:5432 -e POSTGRES_PASSWORD=skillbox -e PO
|
|||
|
||||
#### Commands
|
||||
|
||||
* Create a new teacher demo account on prod
|
||||
##### Create a new teacher demo account on prod
|
||||
```
|
||||
heroku login # if not already logged in
|
||||
heroku run --remote heroku python server/manage.py create_teacher <firstname> <lastname> <email>
|
||||
|
|
@ -49,6 +49,19 @@ Then you can just run in terminal:
|
|||
create_teacher <firstname> <lastname> <email>
|
||||
```
|
||||
|
||||
##### Import a CSV file
|
||||
|
||||
To import a CSV file locally, run:
|
||||
```
|
||||
python manage.py <csv-file>
|
||||
```
|
||||
|
||||
To import a CSV file on prod, first upload the CSV file to some public S3 bucket (or make it publicly available some other way)
|
||||
|
||||
```
|
||||
heroku login # if not already logged in
|
||||
heroku run --remote heroku python server/manage.py import_users --s3 <csv-url>
|
||||
```
|
||||
|
||||
|
||||
### Client
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ aliases:
|
|||
caches:
|
||||
- pip
|
||||
- node
|
||||
artifacts:
|
||||
- client/cypress/**/*.png
|
||||
- client/cypress/**/*.mp4
|
||||
services:
|
||||
- postgres
|
||||
script:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ dist/
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
cypress/videos
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
|
@ -12,3 +13,7 @@ yarn-error.log*
|
|||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"baseUrl": "http://localhost:8000",
|
||||
"video": false
|
||||
"videoUploadOnPasses": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
describe('The Login Page', () => {
|
||||
it('login and redirect to main page', () => {
|
||||
const username = 'test';
|
||||
const password = 'test';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password, true);
|
||||
cy.get('body').contains('Neues Wissen erwerben');
|
||||
});
|
||||
|
||||
it('user sees error message if username is omitted', () => {
|
||||
const username = '';
|
||||
const password = 'test';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password);
|
||||
cy.get('[data-cy=email-local-errors]').contains('ist ein Pflichtfeld');
|
||||
});
|
||||
|
||||
it('user sees error message if password is omitted', () => {
|
||||
const username = 'test';
|
||||
const password = '';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password);
|
||||
cy.get('[data-cy=password-local-errors]').contains('ist ein Pflichtfeld');
|
||||
});
|
||||
|
||||
it('user sees error message if credentials are invalid', () => {
|
||||
const username = 'test';
|
||||
const password = '12345';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password);
|
||||
cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.');
|
||||
});
|
||||
|
||||
it('redirect after login', () => {
|
||||
const username = 'test';
|
||||
const password = 'test';
|
||||
|
||||
cy.visit('/book/topic/berufliche-grundbildung');
|
||||
cy.login(username, password);
|
||||
cy.get('body').contains('Berufliche Grundbildung');
|
||||
});
|
||||
// it('logs in programmatically without using the UI', () => {
|
||||
// cy.visit('/accounts/login/'); // have to get a csrf token by getting the base page first
|
||||
//
|
||||
// const username = 'test';
|
||||
// const password = 'test';
|
||||
//
|
||||
// cy.getCookie('csrftoken').then(token => {
|
||||
// const options = {
|
||||
// url: '/accounts/login/',
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'X-CSRFToken': token.value,
|
||||
// 'content-type': 'multipart/form-data'
|
||||
// },
|
||||
// from: true,
|
||||
// body: {
|
||||
// username: username,
|
||||
// password: password,
|
||||
// // csrfmiddlewaretoken: token.value
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// cy.request(options);
|
||||
// cy.getCookie('sessionid').should('exist');
|
||||
// cy.visit('/');
|
||||
//
|
||||
// cy.get('.start-page__title').should('contain', 'skillbox')
|
||||
//
|
||||
//
|
||||
// // cy.visit('/');
|
||||
// // cy.getCookie('csrftoken')
|
||||
// // .then((csrftoken) => {
|
||||
// // const response = cy.request({
|
||||
// // method: 'POST',
|
||||
// // url: '/login/',
|
||||
// // form: true,
|
||||
// // body: {
|
||||
// // identification: username,
|
||||
// // password: password,
|
||||
// // csrfmiddlewaretoken: csrftoken.value
|
||||
// // }
|
||||
// // });
|
||||
// // });
|
||||
//
|
||||
// });
|
||||
|
||||
|
||||
// })
|
||||
})
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
describe('The Login Page', () => {
|
||||
it('login and redirect to main page', () => {
|
||||
const username = 'test';
|
||||
const password = 'test';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password, true);
|
||||
cy.get('body').contains('Neues Wissen erwerben');
|
||||
});
|
||||
|
||||
it('user sees error message if username is omitted', () => {
|
||||
const username = '';
|
||||
const password = 'test';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password);
|
||||
cy.get('[data-cy=email-local-errors]').contains('E-Mail ist ein Pflichtfeld');
|
||||
});
|
||||
|
||||
it('user sees error message if password is omitted', () => {
|
||||
const username = 'test';
|
||||
const password = '';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password);
|
||||
cy.get('[data-cy=password-local-errors]').contains('Passwort ist ein Pflichtfeld');
|
||||
});
|
||||
|
||||
it('user sees error message if credentials are invalid', () => {
|
||||
const username = 'test';
|
||||
const password = '12345';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password);
|
||||
cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.');
|
||||
});
|
||||
|
||||
it('redirect after login', () => {
|
||||
const username = 'test';
|
||||
const password = 'test';
|
||||
|
||||
cy.visit('/book/topic/berufliche-grundbildung');
|
||||
cy.login(username, password);
|
||||
cy.get('body').contains('Berufliche Grundbildung');
|
||||
});
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
describe('The Regstration Page', () => {
|
||||
// works locally, but not in pipelines.
|
||||
// it('register user', () => {
|
||||
|
||||
// let timestamp = Math.round((new Date()).getTime() / 1000);
|
||||
|
||||
// const firstname = 'pesche';
|
||||
// const lastname = 'peschemann';
|
||||
// const email = `skillboxtest${timestamp}@iterativ.ch`;
|
||||
// const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
|
||||
|
||||
// cy.visit('/register');
|
||||
// cy.register(firstname, lastname, email, licenseKey);
|
||||
// cy.get('.reset__heading').contains('Schauen Sie in Ihr Postfach');
|
||||
// });
|
||||
|
||||
it('user sees error message if firstname is omitted', () => {
|
||||
let timestamp = Math.round((new Date()).getTime() / 1000);
|
||||
const firstname = '';
|
||||
const lastname = 'peschemann';
|
||||
const email = `skillboxtest${timestamp}@iterativ.ch`;
|
||||
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
|
||||
|
||||
cy.visit('/register');
|
||||
cy.register(firstname, lastname, email, licenseKey);
|
||||
cy.get('[data-cy="firstname-local-errors"]').contains('Vorname ist ein Pflichtfeld.');
|
||||
});
|
||||
|
||||
it('user sees error message if lastname is omitted', () => {
|
||||
let timestamp = Math.round((new Date()).getTime() / 1000);
|
||||
const firstname = 'pesche';
|
||||
const lastname = '';
|
||||
const email = `skillboxtest${timestamp}@iterativ.ch`;
|
||||
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
|
||||
|
||||
cy.visit('/register');
|
||||
cy.register(firstname, lastname, email, licenseKey);
|
||||
cy.get('[data-cy="lastname-local-errors"]').contains('Nachname ist ein Pflichtfeld.');
|
||||
});
|
||||
|
||||
it('user sees error message if email is omitted', () => {
|
||||
let timestamp = Math.round((new Date()).getTime() / 1000);
|
||||
const firstname = 'pesche';
|
||||
const lastname = 'peschemann';
|
||||
const email = ``;
|
||||
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
|
||||
|
||||
cy.visit('/register');
|
||||
cy.register(firstname, lastname, email, licenseKey);
|
||||
cy.get('[data-cy="email-local-errors"]').contains('E-Mail ist ein Pflichtfeld.');
|
||||
});
|
||||
|
||||
it('user sees error message if license is omitted', () => {
|
||||
let timestamp = Math.round((new Date()).getTime() / 1000);
|
||||
const firstname = 'pesche';
|
||||
const lastname = 'peschemann';
|
||||
const email = `skillboxtest${timestamp}@iterativ.ch`;
|
||||
const licenseKey = '';
|
||||
|
||||
cy.visit('/register');
|
||||
cy.register(firstname, lastname, email, licenseKey);
|
||||
cy.get('[data-cy="licenseKey-local-errors"]').contains('Lizenz ist ein Pflichtfeld.');
|
||||
});
|
||||
|
||||
it('user sees error message if license key is wrong', () => {
|
||||
let timestamp = Math.round((new Date()).getTime() / 1000);
|
||||
const firstname = 'pesche';
|
||||
const lastname = 'peschemann';
|
||||
const email = `skillboxtest${timestamp}@iterativ.ch`;
|
||||
const licenseKey = 'asdsafsadfsadfasdf';
|
||||
|
||||
cy.visit('/register');
|
||||
cy.register(firstname, lastname, email, licenseKey);
|
||||
cy.get('[data-cy="licenseKey-remote-errors"]').contains('Die angegebenen Lizenz ist unglültig');
|
||||
});
|
||||
|
||||
})
|
||||
|
|
@ -82,3 +82,23 @@ Cypress.Commands.add('changePassword', (oldPassword, newPassword) => {
|
|||
}
|
||||
cy.get('[data-cy=change-password-button]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('register', (firstname, lastname, email, licenseKey) => {
|
||||
if (firstname != '') {
|
||||
cy.get('[data-cy=firstname-input]').type(firstname);
|
||||
}
|
||||
|
||||
if (lastname != '') {
|
||||
cy.get('[data-cy=lastname-input]').type(lastname);
|
||||
}
|
||||
|
||||
if (email != '') {
|
||||
cy.get('[data-cy=email-input]').type(email);
|
||||
}
|
||||
|
||||
if (licenseKey != '') {
|
||||
cy.get('[data-cy=licenseKey-input]').type(licenseKey);
|
||||
}
|
||||
|
||||
cy.get('[data-cy=register-button]').click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8985,8 +8985,7 @@
|
|||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
|
@ -9004,13 +9003,11 @@
|
|||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
|
@ -9023,18 +9020,15 @@
|
|||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
|
@ -9137,8 +9131,7 @@
|
|||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
|
@ -9148,7 +9141,6 @@
|
|||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
|
@ -9161,20 +9153,17 @@
|
|||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
|
|
@ -9191,7 +9180,6 @@
|
|||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
|
@ -9264,8 +9252,7 @@
|
|||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
|
@ -9275,7 +9262,6 @@
|
|||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
|
@ -9351,8 +9337,7 @@
|
|||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
|
@ -9382,7 +9367,6 @@
|
|||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
|
@ -9400,7 +9384,6 @@
|
|||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
|
@ -9439,13 +9422,11 @@
|
|||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -11449,8 +11430,7 @@
|
|||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
|
|
@ -11475,8 +11455,7 @@
|
|||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
|
||||
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
|
||||
import NewObjectiveWizard from '@/components/objective-groups/NewObjectiveWizard';
|
||||
import NewNoteWizard from '@/components/notes/NewNoteWizard';
|
||||
import EditNoteWizard from '@/components/notes/EditNoteWizard';
|
||||
import FullscreenImage from '@/components/FullscreenImage';
|
||||
import FullscreenInfographic from '@/components/FullscreenInfographic';
|
||||
import FullscreenVideo from '@/components/FullscreenVideo';
|
||||
|
|
@ -50,6 +52,8 @@
|
|||
NewProjectEntryWizard,
|
||||
EditProjectEntryWizard,
|
||||
NewObjectiveWizard,
|
||||
NewNoteWizard,
|
||||
EditNoteWizard,
|
||||
FullscreenImage,
|
||||
FullscreenInfographic,
|
||||
FullscreenVideo
|
||||
|
|
|
|||
|
|
@ -18,11 +18,15 @@
|
|||
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
|
||||
<h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4>
|
||||
|
||||
<component v-for="component in contentBlocksWithContentLists.contents"
|
||||
:key="component.id"
|
||||
:is="component.type"
|
||||
v-bind="component">
|
||||
</component>
|
||||
<content-component v-for="component in contentBlocksWithContentLists.contents"
|
||||
:key="component.id"
|
||||
:component="component"
|
||||
:root="root"
|
||||
:parent="contentBlock"
|
||||
:bookmarks="contentBlock.bookmarks"
|
||||
:notes="contentBlock.notes"
|
||||
>
|
||||
</content-component>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -33,22 +37,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import TextBlock from '@/components/content-blocks/TextBlock';
|
||||
import InstrumentWidget from '@/components/content-blocks/InstrumentWidget';
|
||||
import Task from '@/components/content-blocks/Task';
|
||||
import ImageBlock from '@/components/content-blocks/ImageBlock';
|
||||
import ImageUrlBlock from '@/components/content-blocks/ImageUrlBlock';
|
||||
import VideoBlock from '@/components/content-blocks/VideoBlock';
|
||||
import LinkBlock from '@/components/content-blocks/LinkBlock';
|
||||
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
|
||||
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
|
||||
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
||||
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock';
|
||||
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
||||
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
|
||||
import Assignment from '@/components/content-blocks/assignment/Assignment';
|
||||
import Survey from '@/components/content-blocks/SurveyBlock';
|
||||
import Solution from '@/components/content-blocks/Solution';
|
||||
import AddContentButton from '@/components/AddContentButton';
|
||||
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
|
||||
import UserWidget from '@/components/UserWidget';
|
||||
|
|
@ -56,7 +44,7 @@
|
|||
import EyeIcon from '@/components/icons/EyeIcon';
|
||||
import PenIcon from '@/components/icons/PenIcon';
|
||||
import TrashIcon from '@/components/icons/TrashIcon';
|
||||
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug'
|
||||
import ContentComponent from '@/components/content-blocks/ContentComponent';
|
||||
|
||||
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
|
||||
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
|
||||
|
|
@ -77,24 +65,7 @@
|
|||
name: 'content-block',
|
||||
|
||||
components: {
|
||||
'text_block': TextBlock,
|
||||
'basic_knowledge': InstrumentWidget, // for legacy
|
||||
'instrument': InstrumentWidget,
|
||||
'image_block': ImageBlock,
|
||||
'image_url_block': ImageUrlBlock,
|
||||
'video_block': VideoBlock,
|
||||
'link_block': LinkBlock,
|
||||
'document_block': DocumentBlock,
|
||||
'infogram_block': InfogramBlock,
|
||||
'genially_block': GeniallyBlock,
|
||||
'thinglink_block': ThinglinkBlock,
|
||||
'subtitle': SubtitleBlock,
|
||||
'content_list': ContentListBlock,
|
||||
'module_room_slug': ModuleRoomSlug,
|
||||
Survey,
|
||||
Solution,
|
||||
Assignment,
|
||||
Task,
|
||||
ContentComponent,
|
||||
AddContentButton,
|
||||
VisibilityAction,
|
||||
EyeIcon,
|
||||
|
|
@ -181,6 +152,10 @@
|
|||
},
|
||||
hidden() {
|
||||
return isHidden(this.contentBlock, this.schoolClass);
|
||||
},
|
||||
root() {
|
||||
// we need the root content block id, not the generated content block if inside a content list block
|
||||
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="modal__backdrop">
|
||||
<div class="modal" :class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen}">
|
||||
<div class="modal"
|
||||
:class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}">
|
||||
<div class="modal__header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
|
|
@ -32,6 +33,10 @@
|
|||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -68,49 +73,6 @@
|
|||
-ms-grid-rows: auto 1fr 65px;
|
||||
position: relative;
|
||||
|
||||
&--hide-header {
|
||||
grid-template-rows: 1fr 65px;
|
||||
grid-template-areas: "body" "footer";
|
||||
}
|
||||
|
||||
&--hide-header &__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--hide-header &__body {
|
||||
padding: $default-padding;
|
||||
}
|
||||
|
||||
&--fullscreen {
|
||||
width: 95vw;
|
||||
height: auto;
|
||||
grid-template-rows: 1fr;
|
||||
-ms-grid-rows: 1fr;
|
||||
grid-template-areas: "body";
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&--fullscreen &__footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--fullscreen &__body {
|
||||
padding: 0;
|
||||
scrollbar-width: none;
|
||||
margin-right: -5px;
|
||||
|
||||
height: auto;
|
||||
max-height: 95vh;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--fullscreen &__close-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -165,5 +127,59 @@
|
|||
border-top: 1px solid $color-silver-light;
|
||||
padding: 16px $modal-lateral-padding;
|
||||
}
|
||||
|
||||
$parent: &;
|
||||
|
||||
&--hide-header {
|
||||
grid-template-rows: 1fr 65px;
|
||||
grid-template-areas: "body" "footer";
|
||||
|
||||
#{$parent}__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#{$parent}__body {
|
||||
padding: $default-padding;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--fullscreen {
|
||||
width: 95vw;
|
||||
height: auto;
|
||||
grid-template-rows: 1fr;
|
||||
-ms-grid-rows: 1fr;
|
||||
grid-template-areas: "body";
|
||||
overflow: hidden;
|
||||
|
||||
#{$parent}__footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#{$parent}__body {
|
||||
padding: 0;
|
||||
scrollbar-width: none;
|
||||
margin-right: -5px;
|
||||
|
||||
height: auto;
|
||||
max-height: 95vh;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#{$parent}__close-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
height: auto;
|
||||
|
||||
#{$parent}__body {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="content-component" :class="{'content-component--bookmarked': bookmarked}">
|
||||
<bookmark-actions
|
||||
v-if="showBookmarkActions()"
|
||||
@add-note="addNote(component.id)"
|
||||
@edit-note="editNote"
|
||||
@bookmark="bookmarkContent(component.id, !bookmarked)"
|
||||
:bookmarked="bookmarked"
|
||||
:note="note"></bookmark-actions>
|
||||
<component
|
||||
:is="component.type"
|
||||
v-bind="component"
|
||||
:parent="parent"
|
||||
>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from 'vuex';
|
||||
|
||||
import TextBlock from '@/components/content-blocks/TextBlock';
|
||||
import InstrumentWidget from '@/components/content-blocks/InstrumentWidget';
|
||||
import ImageBlock from '@/components/content-blocks/ImageBlock';
|
||||
import ImageUrlBlock from '@/components/content-blocks/ImageUrlBlock';
|
||||
import VideoBlock from '@/components/content-blocks/VideoBlock';
|
||||
import LinkBlock from '@/components/content-blocks/LinkBlock';
|
||||
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
|
||||
import InfogramBlock from '@/components/content-blocks/InfogramBlock';
|
||||
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock';
|
||||
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
||||
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
||||
import ContentListBlock from '@/components/content-blocks/ContentListBlock';
|
||||
import ModuleRoomSlug from '@/components/content-blocks/ModuleRoomSlug';
|
||||
import Assignment from '@/components/content-blocks/assignment/Assignment';
|
||||
import Survey from '@/components/content-blocks/SurveyBlock';
|
||||
import Solution from '@/components/content-blocks/Solution';
|
||||
import BookmarkActions from '@/components/notes/BookmarkActions';
|
||||
|
||||
import UPDATE_CONTENT_BOOKMARK from '@/graphql/gql/mutations/updateContentBookmark.gql';
|
||||
import CONTENT_BLOCK_QUERY from '@/graphql/gql/contentBlockQuery.gql';
|
||||
|
||||
export default {
|
||||
props: ['component', 'parent', 'bookmarks', 'notes', 'root'],
|
||||
|
||||
components: {
|
||||
'text_block': TextBlock,
|
||||
'basic_knowledge': InstrumentWidget, // for legacy
|
||||
'instrument': InstrumentWidget,
|
||||
'image_block': ImageBlock,
|
||||
'image_url_block': ImageUrlBlock,
|
||||
'video_block': VideoBlock,
|
||||
'link_block': LinkBlock,
|
||||
'document_block': DocumentBlock,
|
||||
'infogram_block': InfogramBlock,
|
||||
'genially_block': GeniallyBlock,
|
||||
'subtitle': SubtitleBlock,
|
||||
'content_list': ContentListBlock,
|
||||
'module_room_slug': ModuleRoomSlug,
|
||||
'thinglink_block': ThinglinkBlock,
|
||||
Survey,
|
||||
Solution,
|
||||
Assignment,
|
||||
BookmarkActions
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['editModule']),
|
||||
bookmarked() {
|
||||
return this.bookmarks && !!this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
|
||||
},
|
||||
note() {
|
||||
const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
|
||||
return bookmark && bookmark.note;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addNote(id) {
|
||||
this.$store.dispatch('addNote', {
|
||||
content: id,
|
||||
contentBlock: this.root
|
||||
});
|
||||
},
|
||||
editNote() {
|
||||
this.$store.dispatch('editNote', this.note);
|
||||
},
|
||||
bookmarkContent(uuid, bookmarked) {
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_CONTENT_BOOKMARK,
|
||||
variables: {
|
||||
input: {
|
||||
uuid,
|
||||
contentBlock: this.root,
|
||||
bookmarked
|
||||
}
|
||||
},
|
||||
update: (store, response) => {
|
||||
const query = CONTENT_BLOCK_QUERY;
|
||||
const variables = {id: this.root};
|
||||
const data = store.readQuery({
|
||||
query,
|
||||
variables
|
||||
});
|
||||
|
||||
const bookmarks = data.contentBlock.bookmarks;
|
||||
|
||||
if (bookmarked) {
|
||||
bookmarks.push({
|
||||
note: null,
|
||||
uuid: uuid,
|
||||
__typename: 'ContentBlockBookmarkNode'
|
||||
});
|
||||
} else {
|
||||
let index = bookmarks.findIndex(element => {
|
||||
return element.uuid === uuid;
|
||||
});
|
||||
if (index > -1) {
|
||||
bookmarks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
data.contentBlock.bookmarks = bookmarks;
|
||||
|
||||
store.writeQuery({
|
||||
data,
|
||||
query,
|
||||
variables
|
||||
});
|
||||
},
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
updateContentBookmark: {
|
||||
__typename: 'UpdateContentBookmarkPayload',
|
||||
success: true
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
showBookmarkActions() {
|
||||
return this.component.type !== 'content_list' && this.component.type !== 'basic_knowledge' && !this.editModule;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/_variables.scss";
|
||||
|
||||
.content-component {
|
||||
position: relative;
|
||||
&--bookmarked {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,7 +6,10 @@
|
|||
:key="contentBlock.id"
|
||||
v-for="(contentBlock, index) in contentBlocks">
|
||||
<p class="content-list__numbering">{{alphaIndex(index)}})</p>
|
||||
<content-block :contentBlock="contentBlock"></content-block>
|
||||
<content-block
|
||||
:contentBlock="contentBlock"
|
||||
:parent="parent"
|
||||
></content-block>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
@ -38,7 +41,10 @@
|
|||
return this.contents.map(contentBlock => {
|
||||
return Object.assign({}, contentBlock, {
|
||||
contents: [...contentBlock.value],
|
||||
indent: true
|
||||
indent: true,
|
||||
bookmarks: this.parent.bookmarks,
|
||||
notes: this.parent.notes,
|
||||
root: this.parent.id
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
@ -57,7 +63,7 @@
|
|||
&__item {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
padding: 0 2*15px;
|
||||
padding: 0 0 0 2*15px;
|
||||
}
|
||||
|
||||
&__numbering {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<svg class="add-note-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<path
|
||||
d="M81.5,88.39746H18.7627a2.49981,2.49981,0,0,1-2.5-2.5V35.35352L1.72559,20.71191A2.50054,2.50054,0,0,1,3.5,16.4502h78a2.49981,2.49981,0,0,1,2.5,2.5V85.89746A2.49981,2.49981,0,0,1,81.5,88.39746Zm-60.2373-5H79V21.4502H9.50488L20.53711,32.56152a2.5013,2.5013,0,0,1,.72559,1.76172Z"/>
|
||||
<path d="M64.9209,55.08447H36.18457a2.5,2.5,0,0,1,0-5H64.9209a2.5,2.5,0,0,1,0,5Z"/>
|
||||
<path d="M48.26318,66.95313V38.21582a2.5,2.5,0,0,1,5,0v28.7373a2.5,2.5,0,0,1-5,0Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.add-note-icon {
|
||||
width: 29px;
|
||||
height: 25px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="bookmark-icon">
|
||||
<g :class="{'bookmark-icon--bookmarked': bookmarked}">
|
||||
<path class="bookmark-icon__background"
|
||||
d="M51,67.32872a5.06849,5.06849,0,0,1,2.98875.97385L84,90V11H18V90L48.01333,68.30149A5.064,5.064,0,0,1,51,67.32872Z"
|
||||
/>
|
||||
<path id="bookmark-icon__outline"
|
||||
d="M84.5,93.07715a2.49662,2.49662,0,0,1-1.43359-.45215L51,70.17871,18.93359,92.625A2.49964,2.49964,0,0,1,15,90.57715V11.42285a2.49981,2.49981,0,0,1,2.5-2.5h67a2.49981,2.49981,0,0,1,2.5,2.5v79.1543a2.49947,2.49947,0,0,1-2.5,2.5ZM51,65.15527a4.8673,4.8673,0,0,1,2.80762.88574L82,85.77539V13.92285H20V85.77539L48.19434,66.04A4.863,4.863,0,0,1,51,65.15527Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['bookmarked']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
|
||||
.bookmark-icon {
|
||||
width: 24px;
|
||||
height: 28px;
|
||||
|
||||
&__background {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
$parent: &;
|
||||
|
||||
&--bookmarked {
|
||||
#{$parent}__background {
|
||||
fill: $color-brand;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<svg class="note-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<path
|
||||
d="M81.5,88.39746H18.7627a2.49981,2.49981,0,0,1-2.5-2.5V35.35352L1.72559,20.71191A2.50054,2.50054,0,0,1,3.5,16.4502h78a2.49981,2.49981,0,0,1,2.5,2.5V85.89746A2.49981,2.49981,0,0,1,81.5,88.39746Zm-60.2373-5H79V21.4502H9.50488L20.53711,32.56152a2.5013,2.5013,0,0,1,.72559,1.76172Z"/>
|
||||
<path d="M61.9209,40.92676H39.18457a2.5,2.5,0,0,1,0-5H61.9209a2.5,2.5,0,0,1,0,5Z"/>
|
||||
<path d="M62.13184,55.24219H39.39453a2.5,2.5,0,0,1,0-5h22.7373a2.5,2.5,0,0,1,0,5Z"/>
|
||||
<path d="M62.13184,69.55859H39.39453a2.5,2.5,0,0,1,0-5h22.7373a2.5,2.5,0,0,1,0,5Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.note-icon {
|
||||
width: 29px;
|
||||
height: 25px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="bookmark-actions">
|
||||
<a class="bookmark-actions__action" @click="$emit('bookmark')"
|
||||
:class="{'bookmark-actions__action--bookmarked': bookmarked}">
|
||||
<bookmark-icon :bookmarked="bookmarked"></bookmark-icon>
|
||||
</a>
|
||||
<a class="bookmark-actions__action" v-if="bookmarked && !note" @click="$emit('add-note')">
|
||||
<add-note-icon></add-note-icon>
|
||||
</a>
|
||||
<a class="bookmark-actions__action bookmark-actions__action--noted" @click="$emit('edit-note')" v-if="note">
|
||||
<note-icon></note-icon>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BookmarkIcon from '@/components/icons/BookmarkIcon';
|
||||
import AddNoteIcon from '@/components/icons/AddNoteIcon';
|
||||
import NoteIcon from '@/components/icons/NoteIcon';
|
||||
|
||||
export default {
|
||||
props: ['bookmarked', 'note'],
|
||||
components: {
|
||||
BookmarkIcon,
|
||||
AddNoteIcon,
|
||||
NoteIcon
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.bookmark-actions {
|
||||
height: 100%;
|
||||
min-height: 60px;
|
||||
|
||||
padding: 0 2*$large-spacing;
|
||||
position: absolute;
|
||||
right: -5*$large-spacing;
|
||||
|
||||
display: none;
|
||||
|
||||
@include desktop {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
|
||||
&__action {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
cursor: pointer;
|
||||
width: 26px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&--bookmarked, &--noted {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
$parent: &;
|
||||
|
||||
&:hover {
|
||||
#{$parent}__action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<note-form @save="editNote" @hide="hide" :note="currentNote"></note-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NoteForm from '@/components/notes/NoteForm';
|
||||
|
||||
import UPDATE_NOTE_MUTATION from '@/graphql/gql/mutations/updateNote.gql';
|
||||
import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql';
|
||||
|
||||
import {mapGetters} from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NoteForm
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['currentNote'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
editNote(note) {
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_NOTE_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
note
|
||||
}
|
||||
},
|
||||
refetchQueries: [{
|
||||
query: MODULE_DETAILS_QUERY,
|
||||
variables: {
|
||||
slug: this.$route.params.slug
|
||||
}
|
||||
}]
|
||||
}).then(() => {
|
||||
this.$store.dispatch('hideModal');
|
||||
});
|
||||
},
|
||||
hide() {
|
||||
this.$store.dispatch('hideModal');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<note-form @save="addNote" @hide="hide" :note="note"></note-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NoteForm from '@/components/notes/NoteForm';
|
||||
|
||||
import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql';
|
||||
import CONTENT_BLOCK_QUERY from '@/graphql/gql/contentBlockQuery.gql';
|
||||
|
||||
import {mapGetters} from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NoteForm
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
note: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['currentContent', 'currentContentBlock'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
addNote(note) {
|
||||
const content = this.currentContent;
|
||||
const contentBlock = this.currentContentBlock;
|
||||
const text = note.text;
|
||||
|
||||
this.$apollo.mutate({
|
||||
mutation: ADD_NOTE_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
note: {
|
||||
content,
|
||||
contentBlock,
|
||||
text
|
||||
}
|
||||
}
|
||||
},
|
||||
update: (store, {data: {addNote: {note}}}) => {
|
||||
const query = CONTENT_BLOCK_QUERY;
|
||||
const variables = {id: contentBlock};
|
||||
const data = store.readQuery({
|
||||
query,
|
||||
variables
|
||||
});
|
||||
|
||||
const bookmarks = data.contentBlock.bookmarks;
|
||||
|
||||
let index = bookmarks.findIndex(element => {
|
||||
return element.uuid === content;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
let el = bookmarks[index];
|
||||
el.note = note;
|
||||
bookmarks.splice(index, 1, el);
|
||||
}
|
||||
|
||||
data.contentBlock.bookmarks = bookmarks;
|
||||
|
||||
store.writeQuery({
|
||||
data,
|
||||
query,
|
||||
variables
|
||||
});
|
||||
},
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
addNote: {
|
||||
__typename: 'AddNotePayload',
|
||||
note: {
|
||||
__typename: 'NoteNode',
|
||||
id: -1,
|
||||
text: text
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
this.$store.dispatch('hideModal');
|
||||
});
|
||||
},
|
||||
hide() {
|
||||
this.$store.dispatch('hideModal');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<modal :hide-header="true" :small="true">
|
||||
<modal-input v-on:input="localNote.text = $event"
|
||||
placeholder="Notiz erfassen"
|
||||
:value="localNote.text"
|
||||
></modal-input>
|
||||
<div slot="footer">
|
||||
<a class="button button--primary" data-cy="modal-save-button"
|
||||
@click="$emit('save', localNote)">Speichern</a>
|
||||
<a class="button" @click="$emit('hide')">Abbrechen</a>
|
||||
</div>
|
||||
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '@/components/Modal';
|
||||
import ModalInput from '@/components/ModalInput';
|
||||
|
||||
export default {
|
||||
props: ['note'],
|
||||
|
||||
components: {
|
||||
Modal,
|
||||
ModalInput
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localNote: Object.assign({},
|
||||
{
|
||||
...this.note
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
:class="{ 'skillboxform-input__input--error': errors.has('oldPassword') }"
|
||||
class="change-form__old skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
data-vv-as="Altes Passwort"
|
||||
data-cy="old-password">
|
||||
<small v-if="errors.has('oldPassword') && submitted" class="skillboxform-input__error" data-cy="old-password-local-errors">{{ errors.first('oldPassword') }}</small>
|
||||
<small v-for="error in oldPasswordErrors" :key="error" class=" skillboxform-input__error" data-cy="old-password-remote-errors">{{ error }}</small>
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
name="newPassword"
|
||||
type="text"
|
||||
v-model="newPassword"
|
||||
data-vv-as="Neues Passwort"
|
||||
v-validate="'required|min:8|strongPassword'"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('newPassword') }"
|
||||
class="change-form__new skillbox-input skillboxform-input__input"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ fragment ContentBlockParts on ContentBlockNode {
|
|||
contents
|
||||
userCreated
|
||||
mine
|
||||
bookmarks {
|
||||
uuid
|
||||
note {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
hiddenFor {
|
||||
edges {
|
||||
node {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
mutation AddNote($input: AddNoteInput!) {
|
||||
addNote(input: $input) {
|
||||
note {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
mutation Registration($input: RegistrationInput!){
|
||||
registration(input: $input) {
|
||||
success
|
||||
errors {
|
||||
field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
mutation UpdateContentBookmark($input: UpdateContentBookmarkInput!) {
|
||||
updateContentBookmark(input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
mutation UpdateNote($input: UpdateNoteInput!) {
|
||||
updateNote(input: $input) {
|
||||
note {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import veeDe from 'vee-validate/dist/locale/de';
|
|||
import {dateFilter} from './filters/date-filter';
|
||||
import autoGrow from '@/directives/auto-grow'
|
||||
import clickOutside from '@/directives/click-outside'
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
|
@ -98,13 +99,22 @@ Validator.extend('strongPassword', {
|
|||
return strongRegex.test(value);
|
||||
}
|
||||
});
|
||||
|
||||
Validator.extend('email', {
|
||||
getMessage: field => 'Bitte geben Sie eine gülitge E-Mail an',
|
||||
validate: value => {
|
||||
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
||||
return emailRegex.test(value);
|
||||
}
|
||||
});
|
||||
|
||||
Vue.use(VeeValidate, {
|
||||
locale: 'de'
|
||||
});
|
||||
|
||||
Vue.filter('date', dateFilter);
|
||||
|
||||
/* logged in guard */
|
||||
/* guards */
|
||||
|
||||
function getCookieValue(cookieName) {
|
||||
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
|
||||
|
|
@ -112,18 +122,32 @@ function getCookieValue(cookieName) {
|
|||
return cookieValue ? cookieValue.pop() : '';
|
||||
}
|
||||
|
||||
function redirectIfLoginRequird(to) {
|
||||
function loginRequired(to) {
|
||||
// public pages have the meta.public property set to true
|
||||
return (!to.hasOwnProperty('meta') || !to.meta.hasOwnProperty('public') || !to.meta.public) && getCookieValue('loginStatus') !== 'true';
|
||||
return !to.hasOwnProperty('meta') || !to.meta.hasOwnProperty('public') || !to.meta.public;
|
||||
}
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (redirectIfLoginRequird(to)) {
|
||||
function unauthorizedAccess(to) {
|
||||
return loginRequired(to) && getCookieValue('loginStatus') !== 'true';
|
||||
}
|
||||
|
||||
function redirectStudentsWithoutClass() {
|
||||
return privateApolloClient.query({
|
||||
query: ME_QUERY,
|
||||
}).then(({data}) => data.me.schoolClasses.edges.length === 0 && data.me.permissions.length === 0);
|
||||
}
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (unauthorizedAccess(to)) {
|
||||
const redirectUrl = `/login?redirect=${to.path}`;
|
||||
next(redirectUrl);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
if (to.name !== 'noClass' && loginRequired(to) && await redirectStudentsWithoutClass()) {
|
||||
router.push({name: 'noClass'})
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
/* eslint-disable no-new */
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
import SectionTitleBlock from '@/components/content-blocks/SectionTitleBlock';
|
||||
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
||||
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
||||
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
|
|
@ -48,7 +49,8 @@
|
|||
'document_block': DocumentBlock,
|
||||
'section_title': SectionTitleBlock,
|
||||
'subtitle': SubtitleBlock,
|
||||
'genially_block': GeniallyBlock
|
||||
'genially_block': GeniallyBlock,
|
||||
'thinglink_block': ThinglinkBlock
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="login">
|
||||
<h1 class="login__title">Melden Sie sich jetzt an</h1>
|
||||
<div class="login public-page">
|
||||
<h1 class="login__title public-page__title">Melden Sie sich jetzt an</h1>
|
||||
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit">
|
||||
<div class="login-form__field skillboxform-input">
|
||||
<label for="email" class="skillboxform-input__label">E-Mail</label>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
type="text"
|
||||
v-model="email"
|
||||
v-validate="'required'"
|
||||
data-vv-as="E-Mail"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
|
||||
class="change-form__email skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
id="pw"
|
||||
name="password"
|
||||
type="password"
|
||||
data-vv-as="Passwort"
|
||||
v-model="password"
|
||||
v-validate="'required'"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('password') }"
|
||||
|
|
@ -59,10 +61,11 @@
|
|||
<button class="button button--primary button--big actions__submit" data-cy="login-button">Anmelden</button>
|
||||
<a class="actions__reset text-link" href="/accounts/password_reset/">Passwort vergessen?</a>
|
||||
</div>
|
||||
<!--div class="registration">
|
||||
<p class="registration__text">Haben Sie noch kein Konto?</p>
|
||||
<a class="registration__link text-link" href="/accounts/password_reset/">Jetzt registrieren</a>
|
||||
</div-->
|
||||
<div class="account-link">
|
||||
<p class="account-link__text">Haben Sie noch kein Konto?</p>
|
||||
<router-link class="account-link__link text-link" :to="{name: 'registration'}">Jetzt registrieren
|
||||
</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -92,16 +95,24 @@ export default {
|
|||
store,
|
||||
{
|
||||
data: {
|
||||
login: { success }
|
||||
login
|
||||
}
|
||||
}
|
||||
) {
|
||||
try {
|
||||
if (success) {
|
||||
if (login.success) {
|
||||
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
|
||||
that.$router.push(redirectUrl);
|
||||
} else {
|
||||
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
|
||||
const firstError = login.errors[0];
|
||||
switch (firstError.field) {
|
||||
case 'invalid_credentials':
|
||||
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
|
||||
break;
|
||||
case 'license_inactive':
|
||||
that.loginError = 'Ihre Lizenz ist nicht mehr aktiv.';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
|
|
@ -137,15 +148,6 @@ export default {
|
|||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.login {
|
||||
&__title {
|
||||
margin-top: 48px;
|
||||
font-size: 2.75rem; // 44px
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.text-link {
|
||||
font-family: $sans-serif-font-family;
|
||||
color: $color-brand;
|
||||
|
|
@ -158,12 +160,4 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.registration {
|
||||
margin-top: $large-spacing;
|
||||
&__text {
|
||||
font-family: $sans-serif-font-family;
|
||||
margin-bottom: $small-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,233 @@
|
|||
<template>
|
||||
<div class="registration public-page">
|
||||
<h1 class="registration__title public-page__title">Registrieren Sie ihr persönliches Konto.</h1>
|
||||
<form class="registration__form registration-form" novalidate @submit.prevent="validateBeforeSubmit">
|
||||
<div class="registration-form__field skillboxform-input">
|
||||
<label for="firstname" class="skillboxform-input__label">Vorname</label>
|
||||
<input
|
||||
id="firstname"
|
||||
name="firstname"
|
||||
type="text"
|
||||
v-model="firstname"
|
||||
data-vv-as="Vorname"
|
||||
v-validate="'required'"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('firstname') }"
|
||||
class="change-form__firstname skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
data-cy="firstname-input"
|
||||
/>
|
||||
<small
|
||||
v-if="errors.has('firstname') && submitted"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="firstname-local-errors"
|
||||
>{{ errors.first('firstname') }}</small>
|
||||
<small
|
||||
v-for="error in firstnameErrors"
|
||||
:key="error"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="firstname-remote-errors"
|
||||
>{{ error }}</small>
|
||||
</div>
|
||||
<div class="change-form__field skillboxform-input">
|
||||
<label for="lastname" class="skillboxform-input__label">Nachname</label>
|
||||
<input
|
||||
id="lastname"
|
||||
name="lastname"
|
||||
type="text"
|
||||
v-model="lastname"
|
||||
data-vv-as="Nachname"
|
||||
v-validate="'required'"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('lastname') }"
|
||||
class="change-form__new skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
data-cy="lastname-input"
|
||||
/>
|
||||
<small
|
||||
v-if="errors.has('lastname') && submitted"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="lastname-local-errors"
|
||||
>{{ errors.first('lastname') }}</small>
|
||||
<small
|
||||
v-for="error in lastnameErrors"
|
||||
:key="error"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="lastname-remote-errors"
|
||||
>{{ error }}</small>
|
||||
</div>
|
||||
<div class="change-form__field skillboxform-input">
|
||||
<label for="email" class="skillboxform-input__label">E-Mail</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="text"
|
||||
v-model="email"
|
||||
data-vv-as="E-Mail"
|
||||
v-validate="'required|email'"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
|
||||
class="change-form__new skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
data-cy="email-input"
|
||||
/>
|
||||
<small
|
||||
v-if="errors.has('email') && submitted"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="email-local-errors"
|
||||
>{{ errors.first('email') }}</small>
|
||||
<small
|
||||
v-for="error in emailErrors"
|
||||
:key="error"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="email-remote-errors"
|
||||
>{{ error }}</small>
|
||||
</div>
|
||||
<div class="change-form__field skillboxform-input">
|
||||
<label for="licenseKey" class="skillboxform-input__label">Lizenz</label>
|
||||
<input
|
||||
id="licenseKey"
|
||||
name="licenseKey"
|
||||
type="text"
|
||||
v-model="licenseKey"
|
||||
data-vv-as="Lizenz"
|
||||
v-validate="'required'"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('licenseKey') }"
|
||||
class="change-form__new skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
data-cy="licenseKey-input"
|
||||
/>
|
||||
<small
|
||||
v-if="errors.has('licenseKey') && submitted"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="licenseKey-local-errors"
|
||||
>{{ errors.first('licenseKey') }}</small>
|
||||
<small
|
||||
v-for="error in licenseKeyErrors"
|
||||
:key="error"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="licenseKey-remote-errors"
|
||||
>{{ error }}</small>
|
||||
</div>
|
||||
<div class="skillboxform-input">
|
||||
<small class="skillboxform-input__error" data-cy="registration-error" v-if="registrationError">{{registrationError}}</small>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="button button--primary button--big actions__submit" data-cy="register-button">Jetzt registrieren</button>
|
||||
</div>
|
||||
<div class="account-link">
|
||||
<p class="account-link__text">Haben Sie ein Konto?</p>
|
||||
<router-link class="account-link__link text-link" :to="{name: 'login'}">Jetzt anmelden
|
||||
</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import REGISTRATION_MUTATION from '@/graphql/gql/mutations/registration.gql';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
|
||||
methods: {
|
||||
validateBeforeSubmit() {
|
||||
this.$validator.validate().then(result => {
|
||||
this.submitted = true;
|
||||
let that = this;
|
||||
if (result) {
|
||||
this.$apollo.mutate({
|
||||
client: 'publicClient',
|
||||
mutation: REGISTRATION_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
firstnameInput: this.firstname,
|
||||
lastnameInput: this.lastname,
|
||||
emailInput: this.email,
|
||||
licenseKeyInput: this.licenseKey,
|
||||
}
|
||||
},
|
||||
update(
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
registration: { success, errors }
|
||||
}
|
||||
}
|
||||
) {
|
||||
try {
|
||||
if (success) {
|
||||
window.location.href = '/registration/set-password/done/';
|
||||
} else {
|
||||
errors.forEach(function(error) {
|
||||
switch (error.field) {
|
||||
case 'email':
|
||||
that.emailErrors = ['Die angegebene E-Mail ist bereits registriert.'];
|
||||
break;
|
||||
case 'license_key':
|
||||
that.licenseKeyErrors = ['Die angegebenen Lizenz ist unglültig'];
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
that.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
this.email = '';
|
||||
this.lastname = '';
|
||||
this.firstname = '';
|
||||
this.licenseKey = '';
|
||||
this.firstnameErrors = '';
|
||||
this.lastnameErrors = '';
|
||||
this.emailErrors = '';
|
||||
this.licenseKeyErrors = '';
|
||||
this.registrationError = '';
|
||||
this.submitted = false;
|
||||
this.$validator.reset();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
lastname: '',
|
||||
firstname: '',
|
||||
licenseKey: '',
|
||||
firstnameErrors: '',
|
||||
lastnameErrors: '',
|
||||
emailErrors: '',
|
||||
licenseKeyErrors: '',
|
||||
registrationError: '',
|
||||
submitted: false
|
||||
};
|
||||
}
|
||||
};
|
||||
</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>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="no-class public-page">
|
||||
<h1 class="public-page__title">Sie sind keiner Klasse zugeteilt</h1>
|
||||
<p>Sie können mySkillbox nur verwenden wenn Sie in einer Klasse zugeteilt sind. Aktuell kann Sie nur der mySkillbox-Support einer Klasse zuteilen.</p>
|
||||
<button class="button button--primary button--big logout-button" @click="logout">Abmelden</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
|
||||
|
||||
export default {
|
||||
|
||||
methods: {
|
||||
logout() {
|
||||
this.$apollo.mutate({
|
||||
mutation: LOGOUT_MUTATION,
|
||||
}).then(({data}) => {
|
||||
if (data.logout.success) { location.replace('/') }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
|
||||
.logout-button {
|
||||
margin-top: $large-spacing;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -28,6 +28,8 @@ import surveyPage from '@/pages/survey'
|
|||
import styleGuidePage from '@/pages/styleguide'
|
||||
import moduleRoom from '@/pages/moduleRoom'
|
||||
import login from '@/pages/login'
|
||||
import registration from '@/pages/registration'
|
||||
import waitForClass from '@/pages/waitForClass'
|
||||
|
||||
import store from '@/store/index';
|
||||
|
||||
|
|
@ -117,6 +119,21 @@ const routes = [
|
|||
props: true,
|
||||
meta: {layout: 'simple'}
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
component: registration,
|
||||
name: 'registration',
|
||||
meta: {
|
||||
public: true,
|
||||
layout: 'public',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/no-class',
|
||||
component: waitForClass,
|
||||
name: 'noClass',
|
||||
meta: {layout: 'public'}
|
||||
},
|
||||
{path: '/styleguide', component: styleGuidePage},
|
||||
{path: '*', component: p404}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default new Vuex.Store({
|
|||
showMobileNavigation: false,
|
||||
contentBlockPosition: {},
|
||||
scrollPosition: 0,
|
||||
currentContent: '',
|
||||
currentContentBlock: '',
|
||||
currentRoomEntry: '',
|
||||
parentRoom: null,
|
||||
|
|
@ -19,6 +20,7 @@ export default new Vuex.Store({
|
|||
objectiveGroupType: '',
|
||||
currentObjectiveGroup: '',
|
||||
parentProject: null,
|
||||
currentNote: null,
|
||||
currentProjectEntry: null,
|
||||
imageUrl: '',
|
||||
infographic: {
|
||||
|
|
@ -33,18 +35,17 @@ export default new Vuex.Store({
|
|||
},
|
||||
|
||||
getters: {
|
||||
showModal: state => {
|
||||
return state.showModal
|
||||
},
|
||||
showMobileNavigation: state => {
|
||||
return state.showMobileNavigation
|
||||
},
|
||||
showModal: state => state.showModal,
|
||||
showMobileNavigation: state => state.showMobileNavigation,
|
||||
scrollToAssignmentId: state => state.scrollToAssignmentId,
|
||||
scrollToAssignmentReady: state => state.scrollToAssignmentReady,
|
||||
scrollingToAssignment: state => state.scrollingToAssignment,
|
||||
currentProjectEntry: state => state.currentProjectEntry,
|
||||
editModule: state => state.editModule,
|
||||
currentObjectiveGroup: state => state.currentObjectiveGroup
|
||||
currentObjectiveGroup: state => state.currentObjectiveGroup,
|
||||
currentContent: state => state.currentContent,
|
||||
currentContentBlock: state => state.currentContentBlock,
|
||||
currentNote: state => state.currentNote,
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
@ -58,6 +59,7 @@ export default new Vuex.Store({
|
|||
},
|
||||
resetModalState({commit}) {
|
||||
commit('setCurrentRoomEntry', '');
|
||||
commit('setCurrentContent', '');
|
||||
commit('setCurrentContentBlock', '');
|
||||
commit('setContentBlockPosition', {});
|
||||
commit('setParentRoom', null);
|
||||
|
|
@ -73,6 +75,7 @@ export default new Vuex.Store({
|
|||
type: ''
|
||||
});
|
||||
commit('setVimeoId', null);
|
||||
commit('setCurrentNote', null);
|
||||
},
|
||||
resetContentBlockPosition({commit}) {
|
||||
commit('setContentBlockPosition', {});
|
||||
|
|
@ -122,6 +125,15 @@ export default new Vuex.Store({
|
|||
commit('setCurrentProjectEntry', payload);
|
||||
dispatch('showModal', 'edit-project-entry-wizard');
|
||||
},
|
||||
addNote({commit, dispatch}, payload) {
|
||||
commit('setCurrentContentBlock', payload.contentBlock);
|
||||
commit('setCurrentContent', payload.content);
|
||||
dispatch('showModal', 'new-note-wizard');
|
||||
},
|
||||
editNote({commit, dispatch}, payload) {
|
||||
commit('setCurrentNote', payload);
|
||||
dispatch('showModal', 'edit-note-wizard');
|
||||
},
|
||||
showFullscreenImage({commit, dispatch}, payload) {
|
||||
commit('setImageUrl', payload);
|
||||
dispatch('showModal', 'fullscreen-image');
|
||||
|
|
@ -146,7 +158,7 @@ export default new Vuex.Store({
|
|||
scrollingToAssignment({commit, state, dispatch}, payload) {
|
||||
if (payload && !state.scrollingToAssignment) {
|
||||
commit('setScrollingToAssignment', true);
|
||||
};
|
||||
}
|
||||
|
||||
if (!payload && state.scrollingToAssignment) {
|
||||
commit('setScrollingToAssignment', false);
|
||||
|
|
@ -174,12 +186,18 @@ export default new Vuex.Store({
|
|||
setContentBlockPosition(state, payload) {
|
||||
state.contentBlockPosition = payload;
|
||||
},
|
||||
setCurrentContent(state, payload) {
|
||||
state.currentContent = payload;
|
||||
},
|
||||
setCurrentContentBlock(state, payload) {
|
||||
state.currentContentBlock = payload;
|
||||
},
|
||||
setParentRoom(state, payload) {
|
||||
state.parentRoom = payload;
|
||||
},
|
||||
setCurrentNote(state, payload) {
|
||||
state.currentNote = payload;
|
||||
},
|
||||
setCurrentRoomEntry(state, payload) {
|
||||
state.currentRoomEntry = payload;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
.public-page {
|
||||
&__title {
|
||||
margin-top: 48px;
|
||||
font-size: 2.75rem; // 44px
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.account-link {
|
||||
margin-top: $large-spacing;
|
||||
&__text {
|
||||
font-family: $sans-serif-font-family;
|
||||
margin-bottom: $small-spacing;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,3 +19,4 @@
|
|||
@import "visibility";
|
||||
@import "solutions";
|
||||
@import "password_forms";
|
||||
@import "public-page";
|
||||
|
|
|
|||
|
|
@ -11,16 +11,18 @@ from basicknowledge.queries import BasicKnowledgeQuery
|
|||
from books.schema.mutations.main import BookMutations
|
||||
from books.schema.queries import BookQuery
|
||||
from core.schema.mutations.main import CoreMutations
|
||||
from notes.mutations import NoteMutations
|
||||
from objectives.mutations import ObjectiveMutations
|
||||
from objectives.schema import ObjectivesQuery
|
||||
from portfolio.mutations import PortfolioMutations
|
||||
from portfolio.schema import PortfolioQuery
|
||||
from surveys.schema import SurveysQuery
|
||||
from surveys.mutations import SurveysMutations
|
||||
from surveys.mutations import SurveyMutations
|
||||
from rooms.mutations import RoomMutations
|
||||
from rooms.schema import RoomsQuery, ModuleRoomsQuery
|
||||
from users.schema import AllUsersQuery, UsersQuery
|
||||
from users.mutations import ProfileMutations
|
||||
from registration.mutations_public import RegistrationMutations
|
||||
|
||||
|
||||
class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
|
||||
|
|
@ -33,7 +35,7 @@ class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQ
|
|||
|
||||
|
||||
class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
|
||||
ProfileMutations, SurveysMutations, graphene.ObjectType):
|
||||
ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, graphene.ObjectType):
|
||||
|
||||
if settings.DEBUG:
|
||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ from django.conf import settings
|
|||
from graphene_django.debug import DjangoDebug
|
||||
|
||||
from users.mutations_public import UserMutations
|
||||
from registration.mutations_public import RegistrationMutations
|
||||
|
||||
|
||||
class Mutation(UserMutations, graphene.ObjectType):
|
||||
class Mutation(UserMutations, RegistrationMutations, graphene.ObjectType):
|
||||
|
||||
if settings.DEBUG:
|
||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.0.6 on 2019-10-10 09:52
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notes', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('books', '0014_auto_20190912_1228'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contentblock',
|
||||
name='bookmarks',
|
||||
field=models.ManyToManyField(related_name='bookmarked_content_blocks', through='notes.ContentBlockBookmark', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
|
|
@ -11,8 +11,9 @@ from books.blocks import TextBlock, BasicKnowledgeBlock, LinkBlock, VideoBlock,
|
|||
ThinglinkBlock
|
||||
from books.utils import get_type_and_value
|
||||
from core.wagtail_utils import StrictHierarchyPage
|
||||
from notes.models import ContentBlockBookmark
|
||||
from surveys.models import Survey
|
||||
from users.models import SchoolClass
|
||||
from users.models import SchoolClass, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -34,10 +35,14 @@ class ContentBlock(StrictHierarchyPage):
|
|||
(BASE_SOCIETY, 'Instrument Gesellschaft'),
|
||||
)
|
||||
|
||||
# blocks without owner are visible by default, need to be hidden for each class
|
||||
hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_content_blocks')
|
||||
# blocks with owner are hidden by default, need to be shown for each class
|
||||
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks')
|
||||
user_created = models.BooleanField(default=False)
|
||||
|
||||
bookmarks = models.ManyToManyField(User, through=ContentBlockBookmark, related_name='bookmarked_content_blocks')
|
||||
|
||||
content_blocks = [
|
||||
('text_block', TextBlock()),
|
||||
('basic_knowledge', BasicKnowledgeBlock()),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from books.models import ContentBlock, Chapter, SchoolClass
|
|||
from books.schema.inputs import ContentBlockInput
|
||||
from books.schema.queries import ContentBlockNode
|
||||
from core.utils import set_hidden_for, set_visible_for
|
||||
from notes.models import ContentBlockBookmark
|
||||
from .utils import handle_content_block, set_user_defined_block_type
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from graphene_django.filter import DjangoFilterConnectionField
|
|||
|
||||
from api.utils import get_object
|
||||
from books.utils import are_solutions_enabled_for
|
||||
from notes.models import ContentBlockBookmark
|
||||
from notes.schema import ContentBlockBookmarkNode
|
||||
from rooms.models import ModuleRoomSlug
|
||||
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
||||
|
||||
|
|
@ -24,6 +26,7 @@ def process_module_room_slug_block(content):
|
|||
|
||||
class ContentBlockNode(DjangoObjectType):
|
||||
mine = graphene.Boolean()
|
||||
bookmarks = graphene.List(ContentBlockBookmarkNode)
|
||||
|
||||
class Meta:
|
||||
model = ContentBlock
|
||||
|
|
@ -54,6 +57,12 @@ class ContentBlockNode(DjangoObjectType):
|
|||
self.contents.stream_data = updated_stream_data
|
||||
return self.contents
|
||||
|
||||
def resolve_bookmarks(self, info, **kwargs):
|
||||
return ContentBlockBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
content_block=self
|
||||
)
|
||||
|
||||
|
||||
class ChapterNode(DjangoObjectType):
|
||||
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from django.core.management import BaseCommand
|
|||
from django.db import connection
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from assignments.factories import AssignmentFactory
|
||||
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
|
||||
from core.factories import UserFactory
|
||||
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
|
||||
|
|
@ -103,5 +102,10 @@ class Command(BaseCommand):
|
|||
# ContentBlockFactory.create(parent=chapter, **self.filter_data(content_block_data, 'contents'))
|
||||
ContentBlockFactory.create(parent=chapter, module=module, **content_block_data)
|
||||
|
||||
|
||||
|
||||
# now create all and rooms
|
||||
management.call_command('dummy_rooms', verbosity=0)
|
||||
|
||||
# create license
|
||||
management.call_command('create_dummy_license', verbosity=0)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import csv
|
|||
|
||||
from django.core.management import BaseCommand
|
||||
import os
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from users.models import User, SchoolClass, Role, UserRole
|
||||
|
|
@ -10,47 +11,56 @@ from users.models import User, SchoolClass, Role, UserRole
|
|||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('csv_file')
|
||||
parser.add_argument('--s3', dest='s3', action='store_const', default=False, const=True, help='the file is on a remote server and publicly accessible like e.g. an S3 resource')
|
||||
|
||||
def import_users(self, file_handler):
|
||||
reader = csv.DictReader(file_handler)
|
||||
for row in reader:
|
||||
email = row['Email'].lower().strip()
|
||||
if email == '':
|
||||
self.stdout.write('No e-mail set, skipping')
|
||||
continue
|
||||
school_class_names = [c.strip() for c in row['Klassen'].split(',')]
|
||||
first_name = row['Vorname'].strip()
|
||||
last_name = row['Nachname'].strip()
|
||||
|
||||
self.stdout.write("Creating user {} {}, {}".format(first_name, last_name, email))
|
||||
|
||||
user = User.objects.create_user_with_random_password(first_name, last_name, email)
|
||||
|
||||
if row['Rolle'] == 'Lehrer':
|
||||
self.stdout.write("Assigning teacher role")
|
||||
teacher = Role.objects.get(key='teacher')
|
||||
UserRole.objects.get_or_create(user=user, role=teacher)
|
||||
else:
|
||||
self.stdout.write("Assigning student role")
|
||||
student = Role.objects.get(key='teacher')
|
||||
UserRole.objects.get_or_create(user=user, role=student)
|
||||
|
||||
self.stdout.write("Adding to class(es) {}".format(', '.join(school_class_names)))
|
||||
for school_class_name in school_class_names:
|
||||
school, _ = SchoolClass.objects.get_or_create(name=school_class_name)
|
||||
user.school_classes.add(school)
|
||||
|
||||
self.stdout.write("")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Importing from {}!'.format(options['csv_file']))
|
||||
dir_path = settings.BASE_DIR
|
||||
rel_path = options['csv_file']
|
||||
abs_path = os.path.join(dir_path, rel_path)
|
||||
is_s3 = options['s3']
|
||||
csv_file = options['csv_file']
|
||||
try:
|
||||
with open(abs_path) as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
email = row['Email'].lower().strip()
|
||||
if email == '':
|
||||
self.stdout.write('No e-mail set, skipping')
|
||||
continue
|
||||
school_class_names = [c.strip() for c in row['Klassen'].split(',')]
|
||||
first_name = row['Vorname'].strip()
|
||||
last_name = row['Nachname'].strip()
|
||||
if not is_s3:
|
||||
dir_path = settings.BASE_DIR
|
||||
rel_path = csv_file
|
||||
abs_path = os.path.join(dir_path, rel_path)
|
||||
|
||||
self.stdout.write("Creating user {} {}, {}".format(first_name, last_name, email))
|
||||
|
||||
user, created = User.objects.get_or_create(email=email, username=email)
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.set_password(User.objects.make_random_password())
|
||||
user.save()
|
||||
|
||||
if row['Rolle'] == 'Lehrer':
|
||||
self.stdout.write("Assigning teacher role")
|
||||
teacher = Role.objects.get(key='teacher')
|
||||
UserRole.objects.get_or_create(user=user, role=teacher)
|
||||
else:
|
||||
self.stdout.write("Assigning student role")
|
||||
student = Role.objects.get(key='teacher')
|
||||
UserRole.objects.get_or_create(user=user, role=student)
|
||||
|
||||
self.stdout.write("Adding to class(es) {}".format(', '.join(school_class_names)))
|
||||
for school_class_name in school_class_names:
|
||||
school, _ = SchoolClass.objects.get_or_create(name=school_class_name)
|
||||
user.school_classes.add(school)
|
||||
|
||||
self.stdout.write("")
|
||||
with open(abs_path) as f:
|
||||
self.import_users(f)
|
||||
else:
|
||||
with requests.Session() as s:
|
||||
download = s.get(csv_file)
|
||||
decoded_content = download.content.decode('utf-8')
|
||||
self.import_users(decoded_content.splitlines())
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(e)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ INSTALLED_APPS = [
|
|||
'portfolio',
|
||||
'statistics',
|
||||
'surveys',
|
||||
'notes',
|
||||
'registration',
|
||||
|
||||
'wagtail.contrib.forms',
|
||||
'wagtail.contrib.redirects',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ $base-font-size: 15px;
|
|||
|
||||
$space: 10px;
|
||||
|
||||
$color-brand: #17A887;
|
||||
|
||||
body {
|
||||
font-family: $font-family;
|
||||
font-size: $base-font-size;
|
||||
|
|
@ -115,6 +117,15 @@ input[type=text], input[type=password], input[type=email], select {
|
|||
|
||||
.reset__text {
|
||||
margin-bottom: 52px;
|
||||
line-height: 1.5rem;
|
||||
font-size: 1.125rem;
|
||||
|
||||
a {
|
||||
color: $color-brand;
|
||||
font-family: 'Montserrat', Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.reset__form label {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Setzen Sie Ihr neues Passwort' %}</h2>
|
||||
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
|
||||
<p class="reset__text">{% trans 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten' %}</p>
|
||||
<form method="post" class="mt-1 reset__form">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Sie haben es geschafft' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
|
||||
<p class="reset__text">{% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
|
||||
<p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!-- templates/registration/password_reset_confirm.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
|
||||
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
|
||||
<form method="post" class="mt-1 reset__form">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" name="action">{% trans 'Passwort speichern' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!-- templates/registration/password_reset_form.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Schauen Sie in Ihr Postfach' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2>
|
||||
<p class="reset__text">{% trans 'Wir haben ein E-Mail mit allen weiteren Anweisungen an Sie verschickt. Die E-Mail sollte in Kürze bei Ihnen ankommen.' %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}Sie erhalten diese E-Mail, um Ihr Passwort auf mySkillbox initial zu setzen.{% endblocktrans %}
|
||||
|
||||
{% trans "Bitte öffnen Sie folgende Seite, um Ihr neues Passwort einzugeben:" %}
|
||||
{% block reset_link %}
|
||||
{{ protocol }}://{{ domain }}{% url 'set_password_confirm' uidb64=uid token=token %}
|
||||
{% endblock %}
|
||||
{% trans "Ihr Benutzername lautet:" %} {{ user.get_username }}
|
||||
|
||||
{% trans "Ihr mySkillbox Team" %}
|
||||
|
||||
{% endautoescape %}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<!-- templates/registration/password_reset_form.html -->
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Willkommen bei mySkillbox' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Willkommen bei Myskillbox' %}</h2>
|
||||
<p class="reset__text">{% trans 'Bevor Sie mySkillbox verwenden können, müssen Sie Ihre E-Mail-Adresse bestätigen und ein persönliches Passwort festlegen.' %}</p>
|
||||
<form method="post" class="mt-1 reset__form">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label for="id_email">{% trans 'Geben Sie als erstes hier Ihre E-Mail-Adresse ein:' %}</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
<button class="btn mt-1" type="submit" name="action">{% trans 'E-Mail bestätigen' %}</button>
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Myskillbox: E-Mail bestätigen und Passwort setzen
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
|
||||
<p class="reset__text">% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
|
||||
<p class="reset__text">{% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
|
||||
<p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
{% block body %}
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
|
||||
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
|
||||
<p class="reset__text">{% trans 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten.' %}</p>
|
||||
<form method="post" class="mt-1 reset__form">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}Sie erhalten diese E-Mail, um Ihr Passwort auf {{ site_name }} initial zu setzen.{% endblocktrans %}
|
||||
{% blocktrans %}Sie erhalten diese E-Mail, um Ihr Passwort auf mySkillbox initial zu setzen.{% endblocktrans %}
|
||||
|
||||
{% trans "Bitte öffnen Sie folgende Seite, um Ihr neues Passwort einzugeben:" %}
|
||||
{% block reset_link %}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import json
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from core import settings
|
||||
from core.factories import UserFactory
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ from wagtail.admin import urls as wagtailadmin_urls
|
|||
from wagtail.core import urls as wagtail_urls
|
||||
|
||||
from core import views
|
||||
from core.views import SetPasswordView, SetPasswordDoneView, SetPasswordConfirmView, SetPasswordCompleteView
|
||||
from core.views import LegacySetPasswordView, LegacySetPasswordDoneView, LegacySetPasswordConfirmView,\
|
||||
LegacySetPasswordCompleteView, SetPasswordView, SetPasswordDoneView, SetPasswordConfirmView, SetPasswordCompleteView
|
||||
|
||||
urlpatterns = [
|
||||
# django admin
|
||||
|
|
@ -16,11 +17,20 @@ urlpatterns = [
|
|||
url(r'^accounts/', include('django.contrib.auth.urls')),
|
||||
url(r'^statistics/', include('statistics.urls', namespace='statistics')),
|
||||
|
||||
# legacy - will be removed
|
||||
# set password
|
||||
path('welcome/', SetPasswordView.as_view(), name='set_password'),
|
||||
path('set-password/done/', SetPasswordDoneView.as_view(), name='set_password_done'),
|
||||
path('set-password/<uidb64>/<token>/', SetPasswordConfirmView.as_view(), name='set_password_confirm'),
|
||||
path('set-password/complete/', SetPasswordCompleteView.as_view(), name='set_password_complete'),
|
||||
path('welcome/', LegacySetPasswordView.as_view(), name='set_password'),
|
||||
path('set-password/done/', LegacySetPasswordDoneView.as_view(), name='set_password_done'),
|
||||
path('set-password/<uidb64>/<token>/', LegacySetPasswordConfirmView.as_view(), name='set_password_confirm'),
|
||||
path('set-password/complete/', LegacySetPasswordCompleteView.as_view(), name='set_password_complete'),
|
||||
|
||||
# set password upon registration
|
||||
path('registration/welcome/', SetPasswordView.as_view(), name='registration_set_password'),
|
||||
path('registration/set-password/done/', SetPasswordDoneView.as_view(), name='registration_set_password_done'),
|
||||
path('registration/set-password/<uidb64>/<token>/', SetPasswordConfirmView.as_view(),
|
||||
name='registration_set_password_confirm'),
|
||||
path('registration/set-password/complete/', SetPasswordCompleteView.as_view(),
|
||||
name='registration_set_password_complete'),
|
||||
|
||||
# wagtail
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,31 @@ def home(request):
|
|||
|
||||
|
||||
class SetPasswordView(PasswordResetView):
|
||||
email_template_name = 'registration/registration_set_password_email.html'
|
||||
subject_template_name = 'registration/registration_set_password_subject.txt'
|
||||
success_url = reverse_lazy('registration_set_password_done')
|
||||
template_name = 'registration/registration_set_password_form.html'
|
||||
title = _('Password setzen')
|
||||
|
||||
|
||||
class SetPasswordDoneView(PasswordResetDoneView):
|
||||
template_name = 'registration/registration_set_password_done.html'
|
||||
title = _('Password setzen versandt')
|
||||
|
||||
|
||||
class SetPasswordConfirmView(PasswordResetConfirmView):
|
||||
success_url = reverse_lazy('registration_set_password_complete')
|
||||
template_name = 'registration/registration_set_password_confirm.html'
|
||||
title = _('Gib ein Passwort ein')
|
||||
|
||||
|
||||
class SetPasswordCompleteView(PasswordResetCompleteView):
|
||||
template_name = 'registration/registration_set_password_complete.html'
|
||||
title = _('Passwort setzen erfolgreich')
|
||||
|
||||
|
||||
# legacy
|
||||
class LegacySetPasswordView(PasswordResetView):
|
||||
email_template_name = 'registration/set_password_email.html'
|
||||
subject_template_name = 'registration/set_password_subject.txt'
|
||||
success_url = reverse_lazy('set_password_done')
|
||||
|
|
@ -34,17 +59,17 @@ class SetPasswordView(PasswordResetView):
|
|||
title = _('Password setzen')
|
||||
|
||||
|
||||
class SetPasswordDoneView(PasswordResetDoneView):
|
||||
class LegacySetPasswordDoneView(PasswordResetDoneView):
|
||||
template_name = 'registration/set_password_done.html'
|
||||
title = _('Password setzen versandt')
|
||||
|
||||
|
||||
class SetPasswordConfirmView(PasswordResetConfirmView):
|
||||
class LegacySetPasswordConfirmView(PasswordResetConfirmView):
|
||||
success_url = reverse_lazy('set_password_complete')
|
||||
template_name = 'registration/set_password_confirm.html'
|
||||
title = _('Gib ein Passwort ein')
|
||||
|
||||
|
||||
class SetPasswordCompleteView(PasswordResetCompleteView):
|
||||
class LegacySetPasswordCompleteView(PasswordResetCompleteView):
|
||||
template_name = 'registration/set_password_complete.html'
|
||||
title = _('Passwort setzen erfolgreich')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NotesConfig(AppConfig):
|
||||
name = 'notes'
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import graphene
|
||||
from graphene import InputObjectType
|
||||
|
||||
|
||||
class AddNoteArgument(InputObjectType):
|
||||
content = graphene.UUID(required=True)
|
||||
content_block = graphene.ID(required=True)
|
||||
text = graphene.String(required=True)
|
||||
|
||||
|
||||
class UpdateNoteArgument(InputObjectType):
|
||||
id = graphene.ID(required=True)
|
||||
text = graphene.String(required=True)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 2.0.6 on 2019-10-10 09:52
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('books', '0014_auto_20190912_1228'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentBlockBookmark',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(unique=True)),
|
||||
('content_block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='books.ContentBlock')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Note',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('text', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contentblockbookmark',
|
||||
name='note',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Note'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contentblockbookmark',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
from core.wagtail_utils import StrictHierarchyPage
|
||||
from users.models import User
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
text = models.TextField()
|
||||
|
||||
|
||||
class Bookmark(models.Model):
|
||||
uuid = models.UUIDField(unique=True)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
note = models.OneToOneField(Note, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ContentBlockBookmark(Bookmark):
|
||||
content_block = models.ForeignKey('books.ContentBlock', on_delete=models.CASCADE)
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
from builtins import PermissionError
|
||||
|
||||
import graphene
|
||||
import json
|
||||
from graphene import relay
|
||||
|
||||
from api.utils import get_object
|
||||
from books.models import ContentBlock
|
||||
from notes.inputs import AddNoteArgument, UpdateNoteArgument
|
||||
from notes.models import ContentBlockBookmark, Note
|
||||
from notes.schema import NoteNode
|
||||
|
||||
|
||||
class UpdateContentBookmark(relay.ClientIDMutation):
|
||||
class Input:
|
||||
uuid = graphene.UUID(required=True)
|
||||
content_block = graphene.ID(required=True)
|
||||
bookmarked = graphene.Boolean(required=True)
|
||||
|
||||
success = graphene.Boolean()
|
||||
errors = graphene.String()
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
uuid = kwargs.get('uuid')
|
||||
user = info.context.user
|
||||
content_block_id = kwargs.get('content_block')
|
||||
bookmarked = kwargs.get('bookmarked')
|
||||
|
||||
content_block = get_object(ContentBlock, content_block_id)
|
||||
|
||||
if bookmarked:
|
||||
ContentBlockBookmark.objects.create(
|
||||
content_block=content_block,
|
||||
uuid=uuid,
|
||||
user=user
|
||||
)
|
||||
else:
|
||||
ContentBlockBookmark.objects.get(
|
||||
content_block=content_block,
|
||||
uuid=uuid,
|
||||
user=user
|
||||
).delete()
|
||||
|
||||
return cls(success=True)
|
||||
|
||||
|
||||
class AddNote(relay.ClientIDMutation):
|
||||
class Input:
|
||||
note = graphene.Argument(AddNoteArgument)
|
||||
|
||||
note = graphene.Field(NoteNode)
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
user = info.context.user
|
||||
|
||||
note = kwargs.get('note')
|
||||
content_uuid = note.get('content')
|
||||
content_block_id = note.get('content_block')
|
||||
content_block = get_object(ContentBlock, content_block_id)
|
||||
text = note.get('text')
|
||||
|
||||
bookmark = ContentBlockBookmark.objects.get(
|
||||
content_block=content_block,
|
||||
uuid=content_uuid,
|
||||
user=user
|
||||
)
|
||||
|
||||
bookmark.note = Note.objects.create(text=text)
|
||||
bookmark.save()
|
||||
|
||||
return cls(note=bookmark.note)
|
||||
|
||||
|
||||
class UpdateNote(relay.ClientIDMutation):
|
||||
class Input:
|
||||
note = graphene.Argument(UpdateNoteArgument)
|
||||
|
||||
note = graphene.Field(NoteNode)
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
user = info.context.user
|
||||
|
||||
note = kwargs.get('note')
|
||||
id = note.get('id')
|
||||
text = note.get('text')
|
||||
note = get_object(Note, id)
|
||||
|
||||
if note.contentblockbookmark.user != user:
|
||||
raise PermissionError
|
||||
|
||||
note.text = text
|
||||
note.save()
|
||||
return cls(note=note)
|
||||
|
||||
|
||||
class NoteMutations:
|
||||
add_note = AddNote.Field()
|
||||
update_note = UpdateNote.Field()
|
||||
update_content_bookmark = UpdateContentBookmark.Field()
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from notes.models import Note, ContentBlockBookmark
|
||||
|
||||
|
||||
class NoteNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
|
||||
class Meta:
|
||||
model = Note
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_pk(self, *args, **kwargs):
|
||||
return self.id
|
||||
|
||||
|
||||
class ContentBlockBookmarkNode(DjangoObjectType):
|
||||
# note = graphene.
|
||||
uuid = graphene.UUID()
|
||||
note = graphene.Field(NoteNode)
|
||||
|
||||
class Meta:
|
||||
model = ContentBlockBookmark
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- 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>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ITerativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2019-10-10
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
from django.contrib import admin
|
||||
|
||||
from registration.models import LicenseType, License
|
||||
|
||||
|
||||
@admin.register(LicenseType)
|
||||
class LicenseTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'key', 'for_role', 'active')
|
||||
list_filter = ('for_role', 'active')
|
||||
|
||||
|
||||
@admin.register(License)
|
||||
class LicenseAdmin(admin.ModelAdmin):
|
||||
list_display = ('license_type', 'licensee')
|
||||
list_filter = ('license_type', 'licensee')
|
||||
raw_id_fields = ('licensee',)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- 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'
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# -*- 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 random
|
||||
|
||||
import factory
|
||||
|
||||
from registration.models import LicenseType, License
|
||||
|
||||
|
||||
class LicenseTypeFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = LicenseType
|
||||
|
||||
name = factory.Sequence(lambda n: 'license-{}'.format(n))
|
||||
active = True
|
||||
key = factory.Sequence(lambda n: "license-key-%03d" % n)
|
||||
description = factory.Sequence(lambda n: "Some description %03d" % n)
|
||||
|
||||
|
||||
class LicenseFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = License
|
||||
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ITerativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2019-10-23
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
from django.conf import settings
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ITerativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2019-10-23
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
from django.conf import settings
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ITerativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2019-10-23
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from registration.models import LicenseType
|
||||
from users.models import Role
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
try:
|
||||
role = Role.objects.get(key=Role.objects.TEACHER_KEY)
|
||||
except Role.DoesNotExist:
|
||||
print("LicenseType requires that a Teacher Role exsits")
|
||||
|
||||
LicenseType.objects.create(name='dummy_license',
|
||||
for_role=role,
|
||||
active=True,
|
||||
key='c1fa2e2a-2e27-480d-8469-2e88414c4ad8',
|
||||
description='dummy license')
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- 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.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
from users.managers import RoleManager
|
||||
from users.models import Role, User
|
||||
|
||||
|
||||
class LicenseType(models.Model):
|
||||
|
||||
name = models.CharField(_('License name'), max_length=255, blank=False, null=False)
|
||||
for_role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE)
|
||||
key = models.CharField(max_length=128, blank=False, null=False, unique=True)
|
||||
active = models.BooleanField(_('License active'), default=False)
|
||||
description = models.TextField(_('Description'), default="")
|
||||
|
||||
def is_teacher_license(self):
|
||||
return self.for_role.key == RoleManager.TEACHER_KEY
|
||||
|
||||
def __str__(self):
|
||||
return '%s - role: %s' % (self.name, self.for_role)
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
license_type = models.ForeignKey(LicenseType, blank=False, null=False, on_delete=models.CASCADE)
|
||||
licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# -*- 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 graphene import relay
|
||||
|
||||
from core.views import SetPasswordView
|
||||
from registration.models import License
|
||||
from registration.serializers import RegistrationSerializer
|
||||
from users.models import User, Role, UserRole, SchoolClass
|
||||
|
||||
|
||||
class PublicFieldError(graphene.ObjectType):
|
||||
code = graphene.String()
|
||||
|
||||
|
||||
class MutationError(graphene.ObjectType):
|
||||
field = graphene.String()
|
||||
errors = graphene.List(PublicFieldError)
|
||||
|
||||
|
||||
class Registration(relay.ClientIDMutation):
|
||||
class Input:
|
||||
firstname_input = graphene.String()
|
||||
lastname_input = graphene.String()
|
||||
email_input = graphene.String()
|
||||
license_key_input = graphene.String()
|
||||
|
||||
success = graphene.Boolean()
|
||||
errors = graphene.List(MutationError) # todo: change for consistency
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
first_name = kwargs.get('firstname_input')
|
||||
last_name = kwargs.get('lastname_input')
|
||||
email = kwargs.get('email_input')
|
||||
license_key = kwargs.get('license_key_input')
|
||||
registration_data = {
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'email': email,
|
||||
'license_key': license_key,
|
||||
}
|
||||
|
||||
serializer = RegistrationSerializer(data=registration_data)
|
||||
|
||||
if serializer.is_valid():
|
||||
user = User.objects.create_user_with_random_password(serializer.data['first_name'],
|
||||
serializer.data['last_name'],
|
||||
serializer.data['email'])
|
||||
sb_license = License.objects.create(licensee=user, license_type=serializer.context['license_type'])
|
||||
|
||||
if sb_license.license_type.is_teacher_license():
|
||||
teacher_role = Role.objects.get(key=Role.objects.TEACHER_KEY)
|
||||
UserRole.objects.get_or_create(user=user, role=teacher_role)
|
||||
default_class_name = SchoolClass.generate_default_group_name()
|
||||
default_class = SchoolClass.objects.create(name=default_class_name)
|
||||
user.school_classes.add(default_class)
|
||||
else:
|
||||
student_role = Role.objects.get(key=Role.objects.STUDENT_KEY)
|
||||
UserRole.objects.get_or_create(user=user, role=student_role)
|
||||
|
||||
password_reset_view = SetPasswordView()
|
||||
password_reset_view.request = info.context
|
||||
form = password_reset_view.form_class({'email': user.email})
|
||||
|
||||
if not form.is_valid():
|
||||
return cls(success=False, errors=form.errors)
|
||||
|
||||
password_reset_view.form_valid(form)
|
||||
|
||||
return cls(success=True)
|
||||
|
||||
errors = []
|
||||
for key, value in serializer.errors.items():
|
||||
error = MutationError(field=key, errors=[])
|
||||
for field_error in serializer.errors[key]:
|
||||
error.errors.append(PublicFieldError(code=field_error.code))
|
||||
|
||||
errors.append(error)
|
||||
|
||||
return cls(success=False, errors=errors)
|
||||
|
||||
|
||||
class RegistrationMutations:
|
||||
registration = Registration.Field()
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# -*- 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.contrib.auth import get_user_model
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import CharField, EmailField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from registration.models import License, LicenseType
|
||||
|
||||
|
||||
class RegistrationSerializer(serializers.Serializer):
|
||||
first_name = CharField(allow_blank=False)
|
||||
last_name = CharField(allow_blank=False)
|
||||
email = EmailField(allow_blank=False)
|
||||
license_key = CharField(allow_blank=False)
|
||||
skillbox_license = None
|
||||
|
||||
def validate_email(self, value):
|
||||
lower_email = value.lower()
|
||||
# the email is used as username
|
||||
if len(get_user_model().objects.filter(username=lower_email)) > 0:
|
||||
raise serializers.ValidationError(_(u'Diese E-Mail ist bereits registriert'))
|
||||
elif len(get_user_model().objects.filter(email=lower_email)) > 0:
|
||||
raise serializers.ValidationError(_(u'Dieser E-Mail ist bereits registriert'))
|
||||
else:
|
||||
return lower_email
|
||||
|
||||
def validate_license_key(self, value):
|
||||
license_types = LicenseType.objects.filter(key=value, active=True)
|
||||
if len(license_types) == 0:
|
||||
raise serializers.ValidationError(_(u'Die Lizenznummer ist ungültig'))
|
||||
|
||||
self.context['license_type'] = license_types[0] # Assuming there is just ONE license per key
|
||||
return value
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- 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
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
# -*- 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.core import mail
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase, RequestFactory
|
||||
from graphene.test import Client
|
||||
|
||||
from api.schema import schema
|
||||
from registration.factories import LicenseTypeFactory, LicenseFactory
|
||||
from registration.models import License
|
||||
from users.managers import RoleManager
|
||||
from users.models import Role, User, UserRole, SchoolClass
|
||||
|
||||
|
||||
class RegistrationTests(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role")
|
||||
self.student_role = Role.objects.create(key=Role.objects.STUDENT_KEY, name="Student Role")
|
||||
|
||||
self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role)
|
||||
self.student_license_type = LicenseTypeFactory(for_role=self.student_role)
|
||||
|
||||
self.teacher_license = LicenseFactory(license_type=self.teacher_license_type)
|
||||
self.student_license = LicenseFactory(license_type=self.student_license_type)
|
||||
|
||||
request = RequestFactory().post('/')
|
||||
|
||||
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, first_name, last_name, email, license_key):
|
||||
mutation = '''
|
||||
mutation Registration($input: RegistrationInput!){
|
||||
registration(input: $input) {
|
||||
success
|
||||
errors {
|
||||
field
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
return self.client.execute(mutation, variables={
|
||||
'input': {
|
||||
'firstnameInput': first_name,
|
||||
'lastnameInput': last_name,
|
||||
'emailInput': email,
|
||||
'licenseKeyInput': license_key,
|
||||
}
|
||||
})
|
||||
|
||||
def _assert_user_registration(self, count, email, role_key):
|
||||
users = User.objects.filter(username=self.email)
|
||||
self.assertEqual(len(users), count)
|
||||
user_roles = UserRole.objects.filter(user__email=email, role__key=role_key)
|
||||
self.assertEqual(len(user_roles), count)
|
||||
licenses = License.objects.filter(licensee__email=email, license_type__for_role__key=role_key)
|
||||
self.assertEqual(len(licenses), count)
|
||||
|
||||
def test_user_can_register_as_teacher(self):
|
||||
self._assert_user_registration(0, self.email, RoleManager.TEACHER_KEY)
|
||||
school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse')
|
||||
self.assertEqual(len(school_classes), 0)
|
||||
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.teacher_license_type.key)
|
||||
self.assertTrue(result.get('data').get('registration').get('success'))
|
||||
self._assert_user_registration(1, self.email, RoleManager.TEACHER_KEY)
|
||||
school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse')
|
||||
self.assertEqual(len(school_classes), 1)
|
||||
user = User.objects.get(email=self.email)
|
||||
self.assertTrue(school_classes[0].is_user_in_schoolclass(user))
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, 'Myskillbox: E-Mail bestätigen und Passwort setzen')
|
||||
|
||||
def test_user_can_register_as_student(self):
|
||||
self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY)
|
||||
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
|
||||
self.assertTrue(result.get('data').get('registration').get('success'))
|
||||
self._assert_user_registration(1, self.email, RoleManager.STUDENT_KEY)
|
||||
|
||||
def test_existing_user_cannot_register(self):
|
||||
self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY)
|
||||
self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
|
||||
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
|
||||
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email')
|
||||
|
||||
def test_existing_user_cannot_register_with_uppercase_email(self):
|
||||
self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY)
|
||||
self.make_register_mutation(self.first_name, self.last_name, self.email.upper(), self.student_license_type.key)
|
||||
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
|
||||
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email')
|
||||
|
||||
def test_user_cannot_register_if_firstname_is_missing(self):
|
||||
result = self.make_register_mutation('', self.last_name, self.email, self.teacher_license_type.key)
|
||||
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'first_name')
|
||||
self.assertFalse(result.get('data').get('registration').get('success'))
|
||||
|
||||
def test_user_cannot_register_if_lastname_is_missing(self):
|
||||
result = self.make_register_mutation(self.first_name, '', self.email, self.teacher_license_type.key)
|
||||
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'last_name')
|
||||
self.assertFalse(result.get('data').get('registration').get('success'))
|
||||
|
||||
def test_user_cannot_register_if_email_is_missing(self):
|
||||
result = self.make_register_mutation(self.first_name, self.last_name, '', self.teacher_license_type.key)
|
||||
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email')
|
||||
self.assertFalse(result.get('data').get('registration').get('success'))
|
||||
|
|
@ -33,5 +33,5 @@ class UpdateAnswer(relay.ClientIDMutation):
|
|||
return cls(answer=answer)
|
||||
|
||||
|
||||
class SurveysMutations:
|
||||
class SurveyMutations:
|
||||
update_answer = UpdateAnswer.Field()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from django.contrib.auth.models import Permission
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
|
||||
|
||||
class RoleManager(models.Manager):
|
||||
|
|
@ -78,3 +79,13 @@ class UserRoleManager(models.Manager):
|
|||
user_role = self.model(user=user, role=role)
|
||||
user_role.save()
|
||||
return user_role
|
||||
|
||||
|
||||
class UserManager(DjangoUserManager):
|
||||
def create_user_with_random_password(self, first_name, last_name, email):
|
||||
user, created = self.model.objects.get_or_create(email=email, username=email)
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.set_password(self.model.objects.make_random_password())
|
||||
user.save()
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.0.6 on 2019-10-09 09:05
|
||||
|
||||
from django.db import migrations
|
||||
import users.managers
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0008_auto_20190904_1410'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='user',
|
||||
managers=[
|
||||
('objects', users.managers.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import re
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractUser, Permission
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from users.managers import RoleManager, UserRoleManager
|
||||
from users.managers import RoleManager, UserRoleManager, UserManager
|
||||
|
||||
DEFAULT_SCHOOL_ID = 1
|
||||
|
||||
|
|
@ -14,6 +16,8 @@ class User(AbstractUser):
|
|||
avatar_url = models.CharField(max_length=254, blank=True, default='')
|
||||
email = models.EmailField(_('email address'), unique=True)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def get_role_permissions(self):
|
||||
perms = set()
|
||||
for role in Role.objects.get_roles_for_user(self):
|
||||
|
|
@ -70,6 +74,25 @@ class SchoolClass(models.Model):
|
|||
def __str__(self):
|
||||
return 'SchoolClass {}-{}'.format(self.id, self.name)
|
||||
|
||||
@classmethod
|
||||
def generate_default_group_name(cls):
|
||||
prefix = 'Meine Klasse'
|
||||
prefix_regex = r'Meine Klasse (\d+)'
|
||||
initial_default_group = '{} 1'.format(prefix)
|
||||
my_group_filter = cls.objects.filter(name__startswith=prefix).order_by('-pk')
|
||||
|
||||
if len(my_group_filter) == 0:
|
||||
return initial_default_group
|
||||
|
||||
match = re.search(prefix_regex, my_group_filter[0].name)
|
||||
|
||||
if not match:
|
||||
return initial_default_group
|
||||
|
||||
index = int(match.group(1))
|
||||
|
||||
return '{} {}'.format(prefix, index + 1)
|
||||
|
||||
def is_user_in_schoolclass(self, user):
|
||||
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
|
||||
|
||||
|
|
|
|||
|
|
@ -7,20 +7,16 @@
|
|||
#
|
||||
# Created on 2019-10-01
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
import re
|
||||
|
||||
import graphene
|
||||
from django.contrib.auth import authenticate, login
|
||||
from graphene import relay
|
||||
|
||||
|
||||
class FieldError(graphene.ObjectType):
|
||||
code = graphene.String()
|
||||
from registration.models import License
|
||||
|
||||
|
||||
class MutationError(graphene.ObjectType):
|
||||
class LoginError(graphene.ObjectType):
|
||||
field = graphene.String()
|
||||
errors = graphene.List(FieldError)
|
||||
|
||||
|
||||
class Login(relay.ClientIDMutation):
|
||||
|
|
@ -29,17 +25,30 @@ class Login(relay.ClientIDMutation):
|
|||
password_input = graphene.String()
|
||||
|
||||
success = graphene.Boolean()
|
||||
errors = graphene.List(MutationError) # todo: change for consistency
|
||||
errors = graphene.List(LoginError) # todo: change for consistency
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
|
||||
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input'))
|
||||
if user is not None:
|
||||
login(info.context, user)
|
||||
return cls(success=True, errors=[])
|
||||
else:
|
||||
return cls(success=False, errors=['invalid_credentials'])
|
||||
if user is None:
|
||||
error = LoginError(field='invalid_credentials')
|
||||
return cls(success=False, errors=[error])
|
||||
|
||||
user_license = None
|
||||
|
||||
try:
|
||||
user_license = License.objects.get(licensee=user)
|
||||
except License.DoesNotExist:
|
||||
# current users have no license, allow them to login
|
||||
pass
|
||||
|
||||
if user_license is not None and not user_license.license_type.active:
|
||||
error = LoginError(field='license_inactive')
|
||||
return cls(success=False, errors=[error])
|
||||
|
||||
login(info.context, user)
|
||||
return cls(success=True, errors=[])
|
||||
|
||||
|
||||
class UserMutations:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def validate_old_new_password(value):
|
|||
return value
|
||||
|
||||
|
||||
def validate_strong_email(password):
|
||||
def validate_strong_password(password):
|
||||
|
||||
has_number = re.search('\d', password)
|
||||
has_upper = re.search('[A-Z]', password)
|
||||
|
|
@ -56,7 +56,7 @@ class PasswordSerialzer(serializers.Serializer):
|
|||
new_password = CharField(allow_blank=True, min_length=MIN_PASSWORD_LENGTH)
|
||||
|
||||
def validate_new_password(self, value):
|
||||
return validate_strong_email(value)
|
||||
return validate_strong_password(value)
|
||||
|
||||
def validate_old_password(self, value):
|
||||
return validate_old_password(value, self.context.username)
|
||||
|
|
|
|||
|
|
@ -13,11 +13,15 @@ from graphene.test import Client
|
|||
|
||||
from api.schema_public import schema
|
||||
from core.factories import UserFactory
|
||||
from registration.factories import LicenseTypeFactory, LicenseFactory
|
||||
from users.models import Role
|
||||
|
||||
|
||||
class PasswordResetTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch')
|
||||
self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role")
|
||||
self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role)
|
||||
|
||||
request = RequestFactory().post('/')
|
||||
|
||||
|
|
@ -62,3 +66,25 @@ class PasswordResetTests(TestCase):
|
|||
|
||||
result = self.make_login_mutation(self.user.email, 'test1234')
|
||||
self.assertFalse(result.get('data').get('login').get('success'))
|
||||
|
||||
def test_user_with_active_license_can_login(self):
|
||||
password = 'test123'
|
||||
self.user.set_password(password)
|
||||
self.user.save()
|
||||
|
||||
LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
|
||||
|
||||
result = self.make_login_mutation(self.user.email, password)
|
||||
self.assertTrue(result.get('data').get('login').get('success'))
|
||||
|
||||
def test_user_with_inactive_license_cannot_login(self):
|
||||
password = 'test123'
|
||||
self.user.set_password(password)
|
||||
self.user.save()
|
||||
|
||||
self.teacher_license_type.active = False
|
||||
self.teacher_license_type.save()
|
||||
LicenseFactory(license_type=self.teacher_license_type, licensee=self.user)
|
||||
|
||||
result = self.make_login_mutation(self.user.email, password)
|
||||
self.assertFalse(result.get('data').get('login').get('success'))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from django.contrib.auth import authenticate
|
|||
from users.factories import SchoolClassFactory
|
||||
|
||||
|
||||
class PasswordUpdate(TestCase):
|
||||
class MySchoolClasses(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory(username='aschi')
|
||||
self.another_user = UserFactory(username='pesche')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ITerativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2019-10-10
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
from django.conf import settings
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ITerativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2019-04-09
|
||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||
from django.test import TestCase
|
||||
|
||||
from users.models import SchoolClass
|
||||
|
||||
|
||||
class SchoolClasses(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.prefix = 'Meine Klasse'
|
||||
|
||||
def test_default_class_name_initial(self):
|
||||
class_name = SchoolClass.generate_default_group_name()
|
||||
self.assertEqual('{} 1'.format(self.prefix), class_name)
|
||||
|
||||
def test_default_class_name_initial_with_similar_existing(self):
|
||||
SchoolClass.objects.create(name='{} abc212'.format(self.prefix))
|
||||
class_name = SchoolClass.generate_default_group_name()
|
||||
self.assertEqual('{} 1'.format(self.prefix), class_name)
|
||||
|
||||
def test_default_class_name_if_existing(self):
|
||||
SchoolClass.objects.create(name='{} 1'.format(self.prefix))
|
||||
SchoolClass.objects.create(name='{} 10'.format(self.prefix))
|
||||
class_name = SchoolClass.generate_default_group_name()
|
||||
self.assertEqual('{} 11'.format(self.prefix), class_name)
|
||||
Loading…
Reference in New Issue