commit
0ff9404cbe
|
|
@ -43,5 +43,3 @@ server/media/
|
||||||
|
|
||||||
.coverage
|
.coverage
|
||||||
|
|
||||||
# Cypress screenshots
|
|
||||||
client/cypress/screenshots
|
|
||||||
|
|
|
||||||
3
Pipfile
3
Pipfile
|
|
@ -35,7 +35,8 @@ django-libsass = "*"
|
||||||
bleach = "*"
|
bleach = "*"
|
||||||
newrelic = "*"
|
newrelic = "*"
|
||||||
sentry-sdk = "==0.7.2"
|
sentry-sdk = "==0.7.2"
|
||||||
"django-sendgrid-v5" = "*"
|
"django-sendgrid-v5" = "==0.8.0"
|
||||||
|
python-http-client = "==3.2.1"
|
||||||
coverage = "*"
|
coverage = "*"
|
||||||
graphql-relay = "*"
|
graphql-relay = "*"
|
||||||
ipython = "*"
|
ipython = "*"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "85020e50720b4f71b864719a7c4fae8a0cb4e80352d3bc7eb1e4726bf2405f6d"
|
"sha256": "725dd26226fe58559f67b30a09dec72086dc4681a69f2f2672f5bd87c9ac74b8"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -23,6 +23,14 @@
|
||||||
],
|
],
|
||||||
"version": "==7.0.0"
|
"version": "==7.0.0"
|
||||||
},
|
},
|
||||||
|
"appnope": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
|
||||||
|
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
|
||||||
|
],
|
||||||
|
"markers": "sys_platform == 'darwin'",
|
||||||
|
"version": "==0.1.0"
|
||||||
|
},
|
||||||
"backcall": {
|
"backcall": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||||
|
|
@ -48,18 +56,18 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:778e66711d7de9352b6330692ae44c0ba012559dabb4c39d7416b0135fb335a6",
|
"sha256:08d949fede71c14db8b9b638edaa13831d79daed84e2da27750629fd606bdb57",
|
||||||
"sha256:c8371f1f9c52f64cef1f14a773629fc8434ecca6195b2ef2a429a7bbbf8ecf23"
|
"sha256:4d7c2cc266917cd0ff7e5e039158de80991e21696a2e8bf85201de2d06d7ceea"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.9.228"
|
"version": "==1.10.9"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7c391c46cf1f8d6c04758f84b51337a64136077e4a160eced87551fdcc051669",
|
"sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1",
|
||||||
"sha256:c9148df92ba21a90ea32f2c7f185c31b1c8b8e48417d0ba8cad02b9b3336c09a"
|
"sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3"
|
||||||
],
|
],
|
||||||
"version": "==1.12.228"
|
"version": "==1.13.9"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -122,10 +130,10 @@
|
||||||
},
|
},
|
||||||
"decorator": {
|
"decorator": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
|
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
|
||||||
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
|
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
|
||||||
],
|
],
|
||||||
"version": "==4.4.0"
|
"version": "==4.4.1"
|
||||||
},
|
},
|
||||||
"dj-database-url": {
|
"dj-database-url": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -242,9 +250,9 @@
|
||||||
},
|
},
|
||||||
"draftjs-exporter": {
|
"draftjs-exporter": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:503f222c81de9a0619158d8f88b638f9069af8de233dc020faa782c7a3b22100"
|
"sha256:5839cbc29d7bce2fb99837a404ca40c3a07313f2a20e2700de7ad6aa9a9a18fb"
|
||||||
],
|
],
|
||||||
"version": "==2.1.6"
|
"version": "==2.1.7"
|
||||||
},
|
},
|
||||||
"factory-boy": {
|
"factory-boy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -256,16 +264,16 @@
|
||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1d3f700e8dfcefd6e657118d71405d53e86974448aba78884f119bbd84c0cddf",
|
"sha256:5902379d8df308a204fc11c4f621590ee83975805a6c7b2228203b9defa45250",
|
||||||
"sha256:d5366e120191c5610fceeebfe1c298dc46da0277096f639c6dd7e2eaee0fa547"
|
"sha256:5e8c755c619f332d5ec28b7586389665f136bcf528e165eb925e87c06a63eda7"
|
||||||
],
|
],
|
||||||
"version": "==2.0.1"
|
"version": "==2.0.3"
|
||||||
},
|
},
|
||||||
"future": {
|
"future": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
|
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||||
],
|
],
|
||||||
"version": "==0.17.1"
|
"version": "==0.18.2"
|
||||||
},
|
},
|
||||||
"graphene": {
|
"graphene": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -322,11 +330,11 @@
|
||||||
},
|
},
|
||||||
"ipython": {
|
"ipython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b",
|
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
|
||||||
"sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"
|
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.8.0"
|
"version": "==7.9.0"
|
||||||
},
|
},
|
||||||
"ipython-genutils": {
|
"ipython-genutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -351,30 +359,31 @@
|
||||||
},
|
},
|
||||||
"libsass": {
|
"libsass": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2457723fe04f4e690105f758aa125e809afc840812965095fa3f4edccd6275ef",
|
"sha256:003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2",
|
||||||
"sha256:2974772e7984b27a51a6d91ebc140183ddd574a9663bd02154ddfb75f13a3eed",
|
"sha256:0fd8b4337b3b101c6e6afda9112cc0dc4bacb9133b59d75d65968c7317aa3272",
|
||||||
"sha256:2d067ce4f393fee2ce52bb810a364deac5454dfdb7945d31d1f4265f21f03ab8",
|
"sha256:338e9ae066bf1fde874e335324d5355c52d2081d978b4f74fc59536564b35b08",
|
||||||
"sha256:57d0b99c4e3512233a44141f1bf852570d359724a606dfc4550eccd0f570460d",
|
"sha256:4dcfd561fb100250b89496e1362b96f2cc804f689a59731eb0f94f9a9e144f4a",
|
||||||
"sha256:5b604e4f5befdecc76240c2ba243fd7e23c642ffc2dd86cbfd094a44ead6b08d",
|
"sha256:50778d4be269a021ba2bf42b5b8f6ff3704ab96a82175a052680bddf3ba7cc9f",
|
||||||
"sha256:5dd647ffa1319a2a18572f41fee3bb561d7f77d8d4784074a00b2eb22c61a859",
|
"sha256:6a51393d75f6e3c812785b0fa0b7d67c54258c28011921f204643b55f7355ec0",
|
||||||
"sha256:78f3f14e47612be4fa4b161278f2a3e880a19b6a3367f749e9ae240434b7e7f5",
|
"sha256:74acd9adf506142699dfa292f0e569fdccbd9e7cf619e8226f7117de73566e32",
|
||||||
"sha256:8d423e4b4c0e219488104b4ec4267688dbd816f3ae806beb4201918eff059b2d",
|
"sha256:81a013a4c2a614927fd1ef7a386eddabbba695cbb02defe8f31cf495106e974c",
|
||||||
"sha256:a20473b0427d82e37fa68f0b3a8d219f0bb5ca6d3f7d93b0f5342219285e7064",
|
"sha256:845a9573b25c141164972d498855f4ad29367c09e6d76fad12955ad0e1c83013",
|
||||||
"sha256:c1f76c2a0993914f3c3088e9b6c7031f22e879c5d27a060cdc8c5aa1318eb9b6",
|
"sha256:8b5b6d1a7c4ea1d954e0982b04474cc076286493f6af2d0a13c2e950fbe0be95",
|
||||||
"sha256:c99fbc950f1955e8b6370aafdb9d84d324e4984a2e00a2b47f04dbcc3706a9d1",
|
"sha256:9b59afa0d755089c4165516400a39a289b796b5612eeef5736ab7a1ebf96a67c",
|
||||||
"sha256:cb50f385117535f7671ac7ff3144c1ef0b8e088778c58d269ce6f31b87bfad72",
|
"sha256:a7e685466448c9b1bf98243339793978f654a1151eb5c975f09b83c7a226f4c1",
|
||||||
"sha256:f0f033a8154be60e1a2e1f79ee849ea69a1d62e5d476a78f69e4c7d8fd7c20e1",
|
"sha256:c93df526eeef90b1ea4799c1d33b6cd5aea3e9f4633738fb95c1287c13e6b404",
|
||||||
"sha256:f2572b73b2e13e74b28388ae86c4fabb853ddbfc12279b4444243bd614710ce8",
|
"sha256:e318f06f06847ff49b1f8d086ac9ebce1e63404f7ea329adab92f4f16ba0e00e",
|
||||||
"sha256:f8790db67e00c5bc7be1bdd81ed477563a4b191e839193ecc0c2c5ec679ec481"
|
"sha256:fc5f8336750f76f1bfae82f7e9e89ae71438d26fc4597e3ab4c05ca8fcd41d8a",
|
||||||
|
"sha256:fcb7ab4dc81889e5fc99cafbc2017bc76996f9992fc6b175f7a80edac61d71df"
|
||||||
],
|
],
|
||||||
"version": "==0.19.2"
|
"version": "==0.19.4"
|
||||||
},
|
},
|
||||||
"newrelic": {
|
"newrelic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9d6d3bf2e125410d485fd91279e1b0f50e8f0f5887284b345aed4ec81db33c0a"
|
"sha256:da9adab674d9fe7aa8fbabb6691b33fb7be94a32b6a80548ec7018be9df8ef65"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.0.2.126"
|
"version": "==5.2.1.129"
|
||||||
},
|
},
|
||||||
"parso": {
|
"parso": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -438,11 +447,11 @@
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
|
||||||
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
|
||||||
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
|
||||||
],
|
],
|
||||||
"version": "==2.0.9"
|
"version": "==2.0.10"
|
||||||
},
|
},
|
||||||
"psycopg2": {
|
"psycopg2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -517,14 +526,15 @@
|
||||||
"sha256:c2776054245db376ea26c859b80e9280b1a470b96ed998d60d35951f89bbbe79",
|
"sha256:c2776054245db376ea26c859b80e9280b1a470b96ed998d60d35951f89bbbe79",
|
||||||
"sha256:e455ae0dfd5819ac483f7fecf08ab8693048d9dc47a0a6fe0d4aebf86d9d1d17"
|
"sha256:e455ae0dfd5819ac483f7fecf08ab8693048d9dc47a0a6fe0d4aebf86d9d1d17"
|
||||||
],
|
],
|
||||||
|
"index": "pypi",
|
||||||
"version": "==3.2.1"
|
"version": "==3.2.1"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||||
],
|
],
|
||||||
"version": "==2019.2"
|
"version": "==2019.3"
|
||||||
},
|
},
|
||||||
"raven": {
|
"raven": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -611,17 +621,17 @@
|
||||||
},
|
},
|
||||||
"text-unidecode": {
|
"text-unidecode": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d",
|
"sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
|
||||||
"sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"
|
"sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
|
||||||
],
|
],
|
||||||
"version": "==1.2"
|
"version": "==1.3"
|
||||||
},
|
},
|
||||||
"traitlets": {
|
"traitlets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
|
||||||
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
|
||||||
],
|
],
|
||||||
"version": "==4.3.2"
|
"version": "==4.3.3"
|
||||||
},
|
},
|
||||||
"typing": {
|
"typing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -640,11 +650,11 @@
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.4'",
|
"markers": "python_version >= '3.4'",
|
||||||
"version": "==1.25.3"
|
"version": "==1.25.6"
|
||||||
},
|
},
|
||||||
"wagtail": {
|
"wagtail": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -689,13 +699,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
|
"appnope": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
|
||||||
|
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
|
||||||
|
],
|
||||||
|
"markers": "sys_platform == 'darwin'",
|
||||||
|
"version": "==0.1.0"
|
||||||
|
},
|
||||||
"awscli": {
|
"awscli": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5c36a818130c8393b778e5b98b9bf38985522b62a684782512a2d57340697582",
|
"sha256:4a75cb44dc3dab14bc9bdb0d2731c37a5026bf0afa4acb620fec72c74e62915a",
|
||||||
"sha256:a022dfb7e6cc9ee8540c382b9bdc4637070dbbe2b09f3a74ad4f30719414c0b7"
|
"sha256:90b0b3e91a900e4569bc47f29769522337c46ff50e35f9e4a41830fdb425f000"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.16.238"
|
"version": "==1.16.273"
|
||||||
},
|
},
|
||||||
"backcall": {
|
"backcall": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -706,17 +724,18 @@
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7c391c46cf1f8d6c04758f84b51337a64136077e4a160eced87551fdcc051669",
|
"sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1",
|
||||||
"sha256:c9148df92ba21a90ea32f2c7f185c31b1c8b8e48417d0ba8cad02b9b3336c09a"
|
"sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3"
|
||||||
],
|
],
|
||||||
"version": "==1.12.228"
|
"version": "==1.13.9"
|
||||||
},
|
},
|
||||||
"colorama": {
|
"colorama": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||||
],
|
],
|
||||||
"version": "==0.3.9"
|
"markers": "python_version != '2.6' and python_version != '3.3'",
|
||||||
|
"version": "==0.4.1"
|
||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -758,10 +777,10 @@
|
||||||
},
|
},
|
||||||
"decorator": {
|
"decorator": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
|
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
|
||||||
"sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
|
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
|
||||||
],
|
],
|
||||||
"version": "==4.4.0"
|
"version": "==4.4.1"
|
||||||
},
|
},
|
||||||
"docutils": {
|
"docutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -780,11 +799,11 @@
|
||||||
},
|
},
|
||||||
"ipython": {
|
"ipython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b",
|
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
|
||||||
"sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"
|
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.8.0"
|
"version": "==7.9.0"
|
||||||
},
|
},
|
||||||
"ipython-genutils": {
|
"ipython-genutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -831,11 +850,11 @@
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
|
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
|
||||||
"sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
|
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
|
||||||
"sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
|
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
|
||||||
],
|
],
|
||||||
"version": "==2.0.9"
|
"version": "==2.0.10"
|
||||||
},
|
},
|
||||||
"ptyprocess": {
|
"ptyprocess": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -882,7 +901,7 @@
|
||||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
||||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
||||||
],
|
],
|
||||||
"markers": "python_version != '2.6'",
|
"markers": "python_version != '2.6' and python_version != '3.3'",
|
||||||
"version": "==5.1.2"
|
"version": "==5.1.2"
|
||||||
},
|
},
|
||||||
"rsa": {
|
"rsa": {
|
||||||
|
|
@ -908,18 +927,18 @@
|
||||||
},
|
},
|
||||||
"traitlets": {
|
"traitlets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
|
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
|
||||||
"sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
|
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
|
||||||
],
|
],
|
||||||
"version": "==4.3.2"
|
"version": "==4.3.3"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
|
||||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.4'",
|
"markers": "python_version >= '3.4'",
|
||||||
"version": "==1.25.3"
|
"version": "==1.25.6"
|
||||||
},
|
},
|
||||||
"wcwidth": {
|
"wcwidth": {
|
||||||
"hashes": [
|
"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
|
#### 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 login # if not already logged in
|
||||||
heroku run --remote heroku python server/manage.py create_teacher <firstname> <lastname> <email>
|
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>
|
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
|
### Client
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ aliases:
|
||||||
caches:
|
caches:
|
||||||
- pip
|
- pip
|
||||||
- node
|
- node
|
||||||
|
artifacts:
|
||||||
|
- client/cypress/**/*.png
|
||||||
|
- client/cypress/**/*.mp4
|
||||||
services:
|
services:
|
||||||
- postgres
|
- postgres
|
||||||
script:
|
script:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ dist/
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
cypress/videos
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
|
|
@ -12,3 +13,7 @@ yarn-error.log*
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:8000",
|
"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();
|
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": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
@ -9004,13 +9003,11 @@
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|
@ -9023,18 +9020,15 @@
|
||||||
},
|
},
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
@ -9137,8 +9131,7 @@
|
||||||
},
|
},
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
|
@ -9148,7 +9141,6 @@
|
||||||
"is-fullwidth-code-point": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -9161,20 +9153,17 @@
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
|
@ -9191,7 +9180,6 @@
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
|
@ -9264,8 +9252,7 @@
|
||||||
},
|
},
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
|
@ -9275,7 +9262,6 @@
|
||||||
"once": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
|
@ -9351,8 +9337,7 @@
|
||||||
},
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
|
@ -9382,7 +9367,6 @@
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
|
@ -9400,7 +9384,6 @@
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -9439,13 +9422,11 @@
|
||||||
},
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"bundled": true,
|
"bundled": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -11449,8 +11430,7 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
|
|
@ -11475,8 +11455,7 @@
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
|
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
|
||||||
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
|
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
|
||||||
import NewObjectiveWizard from '@/components/objective-groups/NewObjectiveWizard';
|
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 FullscreenImage from '@/components/FullscreenImage';
|
||||||
import FullscreenInfographic from '@/components/FullscreenInfographic';
|
import FullscreenInfographic from '@/components/FullscreenInfographic';
|
||||||
import FullscreenVideo from '@/components/FullscreenVideo';
|
import FullscreenVideo from '@/components/FullscreenVideo';
|
||||||
|
|
@ -50,6 +52,8 @@
|
||||||
NewProjectEntryWizard,
|
NewProjectEntryWizard,
|
||||||
EditProjectEntryWizard,
|
EditProjectEntryWizard,
|
||||||
NewObjectiveWizard,
|
NewObjectiveWizard,
|
||||||
|
NewNoteWizard,
|
||||||
|
EditNoteWizard,
|
||||||
FullscreenImage,
|
FullscreenImage,
|
||||||
FullscreenInfographic,
|
FullscreenInfographic,
|
||||||
FullscreenVideo
|
FullscreenVideo
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,15 @@
|
||||||
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
|
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
|
||||||
<h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4>
|
<h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4>
|
||||||
|
|
||||||
<component v-for="component in contentBlocksWithContentLists.contents"
|
<content-component v-for="component in contentBlocksWithContentLists.contents"
|
||||||
:key="component.id"
|
:key="component.id"
|
||||||
:is="component.type"
|
:component="component"
|
||||||
v-bind="component">
|
:root="root"
|
||||||
</component>
|
:parent="contentBlock"
|
||||||
|
:bookmarks="contentBlock.bookmarks"
|
||||||
|
:notes="contentBlock.notes"
|
||||||
|
>
|
||||||
|
</content-component>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -33,22 +37,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 AddContentButton from '@/components/AddContentButton';
|
||||||
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
|
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
|
||||||
import UserWidget from '@/components/UserWidget';
|
import UserWidget from '@/components/UserWidget';
|
||||||
|
|
@ -56,7 +44,7 @@
|
||||||
import EyeIcon from '@/components/icons/EyeIcon';
|
import EyeIcon from '@/components/icons/EyeIcon';
|
||||||
import PenIcon from '@/components/icons/PenIcon';
|
import PenIcon from '@/components/icons/PenIcon';
|
||||||
import TrashIcon from '@/components/icons/TrashIcon';
|
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 CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
|
||||||
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
|
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
|
||||||
|
|
@ -77,24 +65,7 @@
|
||||||
name: 'content-block',
|
name: 'content-block',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
'text_block': TextBlock,
|
ContentComponent,
|
||||||
'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,
|
|
||||||
AddContentButton,
|
AddContentButton,
|
||||||
VisibilityAction,
|
VisibilityAction,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
|
@ -181,6 +152,10 @@
|
||||||
},
|
},
|
||||||
hidden() {
|
hidden() {
|
||||||
return isHidden(this.contentBlock, this.schoolClass);
|
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>
|
<template>
|
||||||
<div class="modal__backdrop">
|
<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">
|
<div class="modal__header">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,6 +33,10 @@
|
||||||
fullscreen: {
|
fullscreen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -68,49 +73,6 @@
|
||||||
-ms-grid-rows: auto 1fr 65px;
|
-ms-grid-rows: auto 1fr 65px;
|
||||||
position: relative;
|
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 {
|
&__backdrop {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -165,5 +127,59 @@
|
||||||
border-top: 1px solid $color-silver-light;
|
border-top: 1px solid $color-silver-light;
|
||||||
padding: 16px $modal-lateral-padding;
|
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>
|
</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"
|
:key="contentBlock.id"
|
||||||
v-for="(contentBlock, index) in contentBlocks">
|
v-for="(contentBlock, index) in contentBlocks">
|
||||||
<p class="content-list__numbering">{{alphaIndex(index)}})</p>
|
<p class="content-list__numbering">{{alphaIndex(index)}})</p>
|
||||||
<content-block :contentBlock="contentBlock"></content-block>
|
<content-block
|
||||||
|
:contentBlock="contentBlock"
|
||||||
|
:parent="parent"
|
||||||
|
></content-block>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -38,7 +41,10 @@
|
||||||
return this.contents.map(contentBlock => {
|
return this.contents.map(contentBlock => {
|
||||||
return Object.assign({}, contentBlock, {
|
return Object.assign({}, contentBlock, {
|
||||||
contents: [...contentBlock.value],
|
contents: [...contentBlock.value],
|
||||||
indent: true
|
indent: true,
|
||||||
|
bookmarks: this.parent.bookmarks,
|
||||||
|
notes: this.parent.notes,
|
||||||
|
root: this.parent.id
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +63,7 @@
|
||||||
&__item {
|
&__item {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 2*15px;
|
padding: 0 0 0 2*15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__numbering {
|
&__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="{ 'skillboxform-input__input--error': errors.has('oldPassword') }"
|
||||||
class="change-form__old skillbox-input skillboxform-input__input"
|
class="change-form__old skillbox-input skillboxform-input__input"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
data-vv-as="Altes Passwort"
|
||||||
data-cy="old-password">
|
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-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>
|
<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"
|
name="newPassword"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="newPassword"
|
v-model="newPassword"
|
||||||
|
data-vv-as="Neues Passwort"
|
||||||
v-validate="'required|min:8|strongPassword'"
|
v-validate="'required|min:8|strongPassword'"
|
||||||
:class="{ 'skillboxform-input__input--error': errors.has('newPassword') }"
|
:class="{ 'skillboxform-input__input--error': errors.has('newPassword') }"
|
||||||
class="change-form__new skillbox-input skillboxform-input__input"
|
class="change-form__new skillbox-input skillboxform-input__input"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@ fragment ContentBlockParts on ContentBlockNode {
|
||||||
contents
|
contents
|
||||||
userCreated
|
userCreated
|
||||||
mine
|
mine
|
||||||
|
bookmarks {
|
||||||
|
uuid
|
||||||
|
note {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
hiddenFor {
|
hiddenFor {
|
||||||
edges {
|
edges {
|
||||||
node {
|
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 {dateFilter} from './filters/date-filter';
|
||||||
import autoGrow from '@/directives/auto-grow'
|
import autoGrow from '@/directives/auto-grow'
|
||||||
import clickOutside from '@/directives/click-outside'
|
import clickOutside from '@/directives/click-outside'
|
||||||
|
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
|
@ -98,13 +99,22 @@ Validator.extend('strongPassword', {
|
||||||
return strongRegex.test(value);
|
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, {
|
Vue.use(VeeValidate, {
|
||||||
locale: 'de'
|
locale: 'de'
|
||||||
});
|
});
|
||||||
|
|
||||||
Vue.filter('date', dateFilter);
|
Vue.filter('date', dateFilter);
|
||||||
|
|
||||||
/* logged in guard */
|
/* guards */
|
||||||
|
|
||||||
function getCookieValue(cookieName) {
|
function getCookieValue(cookieName) {
|
||||||
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
|
// 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() : '';
|
return cookieValue ? cookieValue.pop() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectIfLoginRequird(to) {
|
function loginRequired(to) {
|
||||||
// public pages have the meta.public property set to true
|
// 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) => {
|
function unauthorizedAccess(to) {
|
||||||
if (redirectIfLoginRequird(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}`;
|
const redirectUrl = `/login?redirect=${to.path}`;
|
||||||
next(redirectUrl);
|
next(redirectUrl);
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.name !== 'noClass' && loginRequired(to) && await redirectStudentsWithoutClass()) {
|
||||||
|
router.push({name: 'noClass'})
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
import SectionTitleBlock from '@/components/content-blocks/SectionTitleBlock';
|
import SectionTitleBlock from '@/components/content-blocks/SectionTitleBlock';
|
||||||
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
import SubtitleBlock from '@/components/content-blocks/SubtitleBlock';
|
||||||
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
import GeniallyBlock from '@/components/content-blocks/GeniallyBlock';
|
||||||
|
import ThinglinkBlock from '@/components/content-blocks/ThinglinkBlock';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|
@ -48,7 +49,8 @@
|
||||||
'document_block': DocumentBlock,
|
'document_block': DocumentBlock,
|
||||||
'section_title': SectionTitleBlock,
|
'section_title': SectionTitleBlock,
|
||||||
'subtitle': SubtitleBlock,
|
'subtitle': SubtitleBlock,
|
||||||
'genially_block': GeniallyBlock
|
'genially_block': GeniallyBlock,
|
||||||
|
'thinglink_block': ThinglinkBlock
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="login">
|
<div class="login public-page">
|
||||||
<h1 class="login__title">Melden Sie sich jetzt an</h1>
|
<h1 class="login__title public-page__title">Melden Sie sich jetzt an</h1>
|
||||||
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit">
|
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit">
|
||||||
<div class="login-form__field skillboxform-input">
|
<div class="login-form__field skillboxform-input">
|
||||||
<label for="email" class="skillboxform-input__label">E-Mail</label>
|
<label for="email" class="skillboxform-input__label">E-Mail</label>
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
type="text"
|
type="text"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
v-validate="'required'"
|
v-validate="'required'"
|
||||||
|
data-vv-as="E-Mail"
|
||||||
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
|
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
|
||||||
class="change-form__email skillbox-input skillboxform-input__input"
|
class="change-form__email skillbox-input skillboxform-input__input"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
@ -33,6 +34,7 @@
|
||||||
id="pw"
|
id="pw"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
data-vv-as="Passwort"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
v-validate="'required'"
|
v-validate="'required'"
|
||||||
:class="{ 'skillboxform-input__input--error': errors.has('password') }"
|
: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>
|
<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>
|
<a class="actions__reset text-link" href="/accounts/password_reset/">Passwort vergessen?</a>
|
||||||
</div>
|
</div>
|
||||||
<!--div class="registration">
|
<div class="account-link">
|
||||||
<p class="registration__text">Haben Sie noch kein Konto?</p>
|
<p class="account-link__text">Haben Sie noch kein Konto?</p>
|
||||||
<a class="registration__link text-link" href="/accounts/password_reset/">Jetzt registrieren</a>
|
<router-link class="account-link__link text-link" :to="{name: 'registration'}">Jetzt registrieren
|
||||||
</div-->
|
</router-link>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -92,16 +95,24 @@ export default {
|
||||||
store,
|
store,
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
login: { success }
|
login
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (success) {
|
if (login.success) {
|
||||||
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
|
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
|
||||||
that.$router.push(redirectUrl);
|
that.$router.push(redirectUrl);
|
||||||
} else {
|
} 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) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
|
|
@ -137,15 +148,6 @@ export default {
|
||||||
@import "@/styles/_variables.scss";
|
@import "@/styles/_variables.scss";
|
||||||
@import "@/styles/_mixins.scss";
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
.login {
|
|
||||||
&__title {
|
|
||||||
margin-top: 48px;
|
|
||||||
font-size: 2.75rem; // 44px
|
|
||||||
margin-bottom: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
.text-link {
|
||||||
font-family: $sans-serif-font-family;
|
font-family: $sans-serif-font-family;
|
||||||
color: $color-brand;
|
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>
|
</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 styleGuidePage from '@/pages/styleguide'
|
||||||
import moduleRoom from '@/pages/moduleRoom'
|
import moduleRoom from '@/pages/moduleRoom'
|
||||||
import login from '@/pages/login'
|
import login from '@/pages/login'
|
||||||
|
import registration from '@/pages/registration'
|
||||||
|
import waitForClass from '@/pages/waitForClass'
|
||||||
|
|
||||||
import store from '@/store/index';
|
import store from '@/store/index';
|
||||||
|
|
||||||
|
|
@ -117,6 +119,21 @@ const routes = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: {layout: 'simple'}
|
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: '/styleguide', component: styleGuidePage},
|
||||||
{path: '*', component: p404}
|
{path: '*', component: p404}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default new Vuex.Store({
|
||||||
showMobileNavigation: false,
|
showMobileNavigation: false,
|
||||||
contentBlockPosition: {},
|
contentBlockPosition: {},
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
|
currentContent: '',
|
||||||
currentContentBlock: '',
|
currentContentBlock: '',
|
||||||
currentRoomEntry: '',
|
currentRoomEntry: '',
|
||||||
parentRoom: null,
|
parentRoom: null,
|
||||||
|
|
@ -19,6 +20,7 @@ export default new Vuex.Store({
|
||||||
objectiveGroupType: '',
|
objectiveGroupType: '',
|
||||||
currentObjectiveGroup: '',
|
currentObjectiveGroup: '',
|
||||||
parentProject: null,
|
parentProject: null,
|
||||||
|
currentNote: null,
|
||||||
currentProjectEntry: null,
|
currentProjectEntry: null,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
infographic: {
|
infographic: {
|
||||||
|
|
@ -33,18 +35,17 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
showModal: state => {
|
showModal: state => state.showModal,
|
||||||
return state.showModal
|
showMobileNavigation: state => state.showMobileNavigation,
|
||||||
},
|
|
||||||
showMobileNavigation: state => {
|
|
||||||
return state.showMobileNavigation
|
|
||||||
},
|
|
||||||
scrollToAssignmentId: state => state.scrollToAssignmentId,
|
scrollToAssignmentId: state => state.scrollToAssignmentId,
|
||||||
scrollToAssignmentReady: state => state.scrollToAssignmentReady,
|
scrollToAssignmentReady: state => state.scrollToAssignmentReady,
|
||||||
scrollingToAssignment: state => state.scrollingToAssignment,
|
scrollingToAssignment: state => state.scrollingToAssignment,
|
||||||
currentProjectEntry: state => state.currentProjectEntry,
|
currentProjectEntry: state => state.currentProjectEntry,
|
||||||
editModule: state => state.editModule,
|
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: {
|
actions: {
|
||||||
|
|
@ -58,6 +59,7 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
resetModalState({commit}) {
|
resetModalState({commit}) {
|
||||||
commit('setCurrentRoomEntry', '');
|
commit('setCurrentRoomEntry', '');
|
||||||
|
commit('setCurrentContent', '');
|
||||||
commit('setCurrentContentBlock', '');
|
commit('setCurrentContentBlock', '');
|
||||||
commit('setContentBlockPosition', {});
|
commit('setContentBlockPosition', {});
|
||||||
commit('setParentRoom', null);
|
commit('setParentRoom', null);
|
||||||
|
|
@ -73,6 +75,7 @@ export default new Vuex.Store({
|
||||||
type: ''
|
type: ''
|
||||||
});
|
});
|
||||||
commit('setVimeoId', null);
|
commit('setVimeoId', null);
|
||||||
|
commit('setCurrentNote', null);
|
||||||
},
|
},
|
||||||
resetContentBlockPosition({commit}) {
|
resetContentBlockPosition({commit}) {
|
||||||
commit('setContentBlockPosition', {});
|
commit('setContentBlockPosition', {});
|
||||||
|
|
@ -122,6 +125,15 @@ export default new Vuex.Store({
|
||||||
commit('setCurrentProjectEntry', payload);
|
commit('setCurrentProjectEntry', payload);
|
||||||
dispatch('showModal', 'edit-project-entry-wizard');
|
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) {
|
showFullscreenImage({commit, dispatch}, payload) {
|
||||||
commit('setImageUrl', payload);
|
commit('setImageUrl', payload);
|
||||||
dispatch('showModal', 'fullscreen-image');
|
dispatch('showModal', 'fullscreen-image');
|
||||||
|
|
@ -146,7 +158,7 @@ export default new Vuex.Store({
|
||||||
scrollingToAssignment({commit, state, dispatch}, payload) {
|
scrollingToAssignment({commit, state, dispatch}, payload) {
|
||||||
if (payload && !state.scrollingToAssignment) {
|
if (payload && !state.scrollingToAssignment) {
|
||||||
commit('setScrollingToAssignment', true);
|
commit('setScrollingToAssignment', true);
|
||||||
};
|
}
|
||||||
|
|
||||||
if (!payload && state.scrollingToAssignment) {
|
if (!payload && state.scrollingToAssignment) {
|
||||||
commit('setScrollingToAssignment', false);
|
commit('setScrollingToAssignment', false);
|
||||||
|
|
@ -174,12 +186,18 @@ export default new Vuex.Store({
|
||||||
setContentBlockPosition(state, payload) {
|
setContentBlockPosition(state, payload) {
|
||||||
state.contentBlockPosition = payload;
|
state.contentBlockPosition = payload;
|
||||||
},
|
},
|
||||||
|
setCurrentContent(state, payload) {
|
||||||
|
state.currentContent = payload;
|
||||||
|
},
|
||||||
setCurrentContentBlock(state, payload) {
|
setCurrentContentBlock(state, payload) {
|
||||||
state.currentContentBlock = payload;
|
state.currentContentBlock = payload;
|
||||||
},
|
},
|
||||||
setParentRoom(state, payload) {
|
setParentRoom(state, payload) {
|
||||||
state.parentRoom = payload;
|
state.parentRoom = payload;
|
||||||
},
|
},
|
||||||
|
setCurrentNote(state, payload) {
|
||||||
|
state.currentNote = payload;
|
||||||
|
},
|
||||||
setCurrentRoomEntry(state, payload) {
|
setCurrentRoomEntry(state, payload) {
|
||||||
state.currentRoomEntry = 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 "visibility";
|
||||||
@import "solutions";
|
@import "solutions";
|
||||||
@import "password_forms";
|
@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.mutations.main import BookMutations
|
||||||
from books.schema.queries import BookQuery
|
from books.schema.queries import BookQuery
|
||||||
from core.schema.mutations.main import CoreMutations
|
from core.schema.mutations.main import CoreMutations
|
||||||
|
from notes.mutations import NoteMutations
|
||||||
from objectives.mutations import ObjectiveMutations
|
from objectives.mutations import ObjectiveMutations
|
||||||
from objectives.schema import ObjectivesQuery
|
from objectives.schema import ObjectivesQuery
|
||||||
from portfolio.mutations import PortfolioMutations
|
from portfolio.mutations import PortfolioMutations
|
||||||
from portfolio.schema import PortfolioQuery
|
from portfolio.schema import PortfolioQuery
|
||||||
from surveys.schema import SurveysQuery
|
from surveys.schema import SurveysQuery
|
||||||
from surveys.mutations import SurveysMutations
|
from surveys.mutations import SurveyMutations
|
||||||
from rooms.mutations import RoomMutations
|
from rooms.mutations import RoomMutations
|
||||||
from rooms.schema import RoomsQuery, ModuleRoomsQuery
|
from rooms.schema import RoomsQuery, ModuleRoomsQuery
|
||||||
from users.schema import AllUsersQuery, UsersQuery
|
from users.schema import AllUsersQuery, UsersQuery
|
||||||
from users.mutations import ProfileMutations
|
from users.mutations import ProfileMutations
|
||||||
|
from registration.mutations_public import RegistrationMutations
|
||||||
|
|
||||||
|
|
||||||
class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
|
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,
|
class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
|
||||||
ProfileMutations, SurveysMutations, graphene.ObjectType):
|
ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, graphene.ObjectType):
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
debug = graphene.Field(DjangoDebug, name='__debug')
|
debug = graphene.Field(DjangoDebug, name='__debug')
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ from django.conf import settings
|
||||||
from graphene_django.debug import DjangoDebug
|
from graphene_django.debug import DjangoDebug
|
||||||
|
|
||||||
from users.mutations_public import UserMutations
|
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:
|
if settings.DEBUG:
|
||||||
debug = graphene.Field(DjangoDebug, name='__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
|
ThinglinkBlock
|
||||||
from books.utils import get_type_and_value
|
from books.utils import get_type_and_value
|
||||||
from core.wagtail_utils import StrictHierarchyPage
|
from core.wagtail_utils import StrictHierarchyPage
|
||||||
|
from notes.models import ContentBlockBookmark
|
||||||
from surveys.models import Survey
|
from surveys.models import Survey
|
||||||
from users.models import SchoolClass
|
from users.models import SchoolClass, User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -34,10 +35,14 @@ class ContentBlock(StrictHierarchyPage):
|
||||||
(BASE_SOCIETY, 'Instrument Gesellschaft'),
|
(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')
|
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')
|
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_content_blocks')
|
||||||
user_created = models.BooleanField(default=False)
|
user_created = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
bookmarks = models.ManyToManyField(User, through=ContentBlockBookmark, related_name='bookmarked_content_blocks')
|
||||||
|
|
||||||
content_blocks = [
|
content_blocks = [
|
||||||
('text_block', TextBlock()),
|
('text_block', TextBlock()),
|
||||||
('basic_knowledge', BasicKnowledgeBlock()),
|
('basic_knowledge', BasicKnowledgeBlock()),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from books.models import ContentBlock, Chapter, SchoolClass
|
||||||
from books.schema.inputs import ContentBlockInput
|
from books.schema.inputs import ContentBlockInput
|
||||||
from books.schema.queries import ContentBlockNode
|
from books.schema.queries import ContentBlockNode
|
||||||
from core.utils import set_hidden_for, set_visible_for
|
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
|
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 api.utils import get_object
|
||||||
from books.utils import are_solutions_enabled_for
|
from books.utils import are_solutions_enabled_for
|
||||||
|
from notes.models import ContentBlockBookmark
|
||||||
|
from notes.schema import ContentBlockBookmarkNode
|
||||||
from rooms.models import ModuleRoomSlug
|
from rooms.models import ModuleRoomSlug
|
||||||
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
||||||
|
|
||||||
|
|
@ -24,6 +26,7 @@ def process_module_room_slug_block(content):
|
||||||
|
|
||||||
class ContentBlockNode(DjangoObjectType):
|
class ContentBlockNode(DjangoObjectType):
|
||||||
mine = graphene.Boolean()
|
mine = graphene.Boolean()
|
||||||
|
bookmarks = graphene.List(ContentBlockBookmarkNode)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentBlock
|
model = ContentBlock
|
||||||
|
|
@ -54,6 +57,12 @@ class ContentBlockNode(DjangoObjectType):
|
||||||
self.contents.stream_data = updated_stream_data
|
self.contents.stream_data = updated_stream_data
|
||||||
return self.contents
|
return self.contents
|
||||||
|
|
||||||
|
def resolve_bookmarks(self, info, **kwargs):
|
||||||
|
return ContentBlockBookmark.objects.filter(
|
||||||
|
user=info.context.user,
|
||||||
|
content_block=self
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChapterNode(DjangoObjectType):
|
class ChapterNode(DjangoObjectType):
|
||||||
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from django.core.management import BaseCommand
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from wagtail.core.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from assignments.factories import AssignmentFactory
|
|
||||||
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
|
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
|
||||||
from core.factories import UserFactory
|
from core.factories import UserFactory
|
||||||
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
|
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
|
||||||
|
|
@ -103,5 +102,10 @@ class Command(BaseCommand):
|
||||||
# ContentBlockFactory.create(parent=chapter, **self.filter_data(content_block_data, 'contents'))
|
# ContentBlockFactory.create(parent=chapter, **self.filter_data(content_block_data, 'contents'))
|
||||||
ContentBlockFactory.create(parent=chapter, module=module, **content_block_data)
|
ContentBlockFactory.create(parent=chapter, module=module, **content_block_data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# now create all and rooms
|
# now create all and rooms
|
||||||
management.call_command('dummy_rooms', verbosity=0)
|
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
|
from django.core.management import BaseCommand
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from users.models import User, SchoolClass, Role, UserRole
|
from users.models import User, SchoolClass, Role, UserRole
|
||||||
|
|
@ -10,47 +11,56 @@ from users.models import User, SchoolClass, Role, UserRole
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('csv_file')
|
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):
|
def handle(self, *args, **options):
|
||||||
self.stdout.write('Importing from {}!'.format(options['csv_file']))
|
self.stdout.write('Importing from {}!'.format(options['csv_file']))
|
||||||
dir_path = settings.BASE_DIR
|
is_s3 = options['s3']
|
||||||
rel_path = options['csv_file']
|
csv_file = options['csv_file']
|
||||||
abs_path = os.path.join(dir_path, rel_path)
|
|
||||||
try:
|
try:
|
||||||
with open(abs_path) as f:
|
if not is_s3:
|
||||||
reader = csv.DictReader(f)
|
dir_path = settings.BASE_DIR
|
||||||
for row in reader:
|
rel_path = csv_file
|
||||||
email = row['Email'].lower().strip()
|
abs_path = os.path.join(dir_path, rel_path)
|
||||||
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))
|
with open(abs_path) as f:
|
||||||
|
self.import_users(f)
|
||||||
user, created = User.objects.get_or_create(email=email, username=email)
|
else:
|
||||||
user.first_name = first_name
|
with requests.Session() as s:
|
||||||
user.last_name = last_name
|
download = s.get(csv_file)
|
||||||
user.set_password(User.objects.make_random_password())
|
decoded_content = download.content.decode('utf-8')
|
||||||
user.save()
|
self.import_users(decoded_content.splitlines())
|
||||||
|
|
||||||
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("")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.stdout.write(e)
|
self.stdout.write(e)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ INSTALLED_APPS = [
|
||||||
'portfolio',
|
'portfolio',
|
||||||
'statistics',
|
'statistics',
|
||||||
'surveys',
|
'surveys',
|
||||||
|
'notes',
|
||||||
|
'registration',
|
||||||
|
|
||||||
'wagtail.contrib.forms',
|
'wagtail.contrib.forms',
|
||||||
'wagtail.contrib.redirects',
|
'wagtail.contrib.redirects',
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ $base-font-size: 15px;
|
||||||
|
|
||||||
$space: 10px;
|
$space: 10px;
|
||||||
|
|
||||||
|
$color-brand: #17A887;
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: $font-family;
|
font-family: $font-family;
|
||||||
font-size: $base-font-size;
|
font-size: $base-font-size;
|
||||||
|
|
@ -115,6 +117,15 @@ input[type=text], input[type=password], input[type=email], select {
|
||||||
|
|
||||||
.reset__text {
|
.reset__text {
|
||||||
margin-bottom: 52px;
|
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 {
|
.reset__form label {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="reset">
|
<div class="reset">
|
||||||
<h2 class="reset__heading">{% trans 'Setzen Sie Ihr neues Passwort' %}</h2>
|
<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">
|
<form method="post" class="mt-1 reset__form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p }}
|
{{ 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 %}
|
{% block body %}
|
||||||
<div class="reset">
|
<div class="reset">
|
||||||
<h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
|
<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>
|
<p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="reset">
|
<div class="reset">
|
||||||
<h2 class="reset__heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
|
<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">
|
<form method="post" class="mt-1 reset__form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p }}
|
{{ form.as_p }}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}{% autoescape off %}
|
{% 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:" %}
|
{% trans "Bitte öffnen Sie folgende Seite, um Ihr neues Passwort einzugeben:" %}
|
||||||
{% block reset_link %}
|
{% block reset_link %}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.test.utils import override_settings
|
|
||||||
|
|
||||||
from core import settings
|
|
||||||
from core.factories import UserFactory
|
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 wagtail.core import urls as wagtail_urls
|
||||||
|
|
||||||
from core import views
|
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 = [
|
urlpatterns = [
|
||||||
# django admin
|
# django admin
|
||||||
|
|
@ -16,11 +17,20 @@ urlpatterns = [
|
||||||
url(r'^accounts/', include('django.contrib.auth.urls')),
|
url(r'^accounts/', include('django.contrib.auth.urls')),
|
||||||
url(r'^statistics/', include('statistics.urls', namespace='statistics')),
|
url(r'^statistics/', include('statistics.urls', namespace='statistics')),
|
||||||
|
|
||||||
|
# legacy - will be removed
|
||||||
# set password
|
# set password
|
||||||
path('welcome/', SetPasswordView.as_view(), name='set_password'),
|
path('welcome/', LegacySetPasswordView.as_view(), name='set_password'),
|
||||||
path('set-password/done/', SetPasswordDoneView.as_view(), name='set_password_done'),
|
path('set-password/done/', LegacySetPasswordDoneView.as_view(), name='set_password_done'),
|
||||||
path('set-password/<uidb64>/<token>/', SetPasswordConfirmView.as_view(), name='set_password_confirm'),
|
path('set-password/<uidb64>/<token>/', LegacySetPasswordConfirmView.as_view(), name='set_password_confirm'),
|
||||||
path('set-password/complete/', SetPasswordCompleteView.as_view(), name='set_password_complete'),
|
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
|
# wagtail
|
||||||
url(r'^cms/', include(wagtailadmin_urls)),
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,31 @@ def home(request):
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordView(PasswordResetView):
|
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'
|
email_template_name = 'registration/set_password_email.html'
|
||||||
subject_template_name = 'registration/set_password_subject.txt'
|
subject_template_name = 'registration/set_password_subject.txt'
|
||||||
success_url = reverse_lazy('set_password_done')
|
success_url = reverse_lazy('set_password_done')
|
||||||
|
|
@ -34,17 +59,17 @@ class SetPasswordView(PasswordResetView):
|
||||||
title = _('Password setzen')
|
title = _('Password setzen')
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordDoneView(PasswordResetDoneView):
|
class LegacySetPasswordDoneView(PasswordResetDoneView):
|
||||||
template_name = 'registration/set_password_done.html'
|
template_name = 'registration/set_password_done.html'
|
||||||
title = _('Password setzen versandt')
|
title = _('Password setzen versandt')
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordConfirmView(PasswordResetConfirmView):
|
class LegacySetPasswordConfirmView(PasswordResetConfirmView):
|
||||||
success_url = reverse_lazy('set_password_complete')
|
success_url = reverse_lazy('set_password_complete')
|
||||||
template_name = 'registration/set_password_confirm.html'
|
template_name = 'registration/set_password_confirm.html'
|
||||||
title = _('Gib ein Passwort ein')
|
title = _('Gib ein Passwort ein')
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordCompleteView(PasswordResetCompleteView):
|
class LegacySetPasswordCompleteView(PasswordResetCompleteView):
|
||||||
template_name = 'registration/set_password_complete.html'
|
template_name = 'registration/set_password_complete.html'
|
||||||
title = _('Passwort setzen erfolgreich')
|
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)
|
return cls(answer=answer)
|
||||||
|
|
||||||
|
|
||||||
class SurveysMutations:
|
class SurveyMutations:
|
||||||
update_answer = UpdateAnswer.Field()
|
update_answer = UpdateAnswer.Field()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from django.contrib.auth.models import Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||||
|
|
||||||
|
|
||||||
class RoleManager(models.Manager):
|
class RoleManager(models.Manager):
|
||||||
|
|
@ -78,3 +79,13 @@ class UserRoleManager(models.Manager):
|
||||||
user_role = self.model(user=user, role=role)
|
user_role = self.model(user=user, role=role)
|
||||||
user_role.save()
|
user_role.save()
|
||||||
return user_role
|
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 import get_user_model
|
||||||
from django.contrib.auth.models import AbstractUser, Permission
|
from django.contrib.auth.models import AbstractUser, Permission
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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
|
DEFAULT_SCHOOL_ID = 1
|
||||||
|
|
||||||
|
|
@ -14,6 +16,8 @@ class User(AbstractUser):
|
||||||
avatar_url = models.CharField(max_length=254, blank=True, default='')
|
avatar_url = models.CharField(max_length=254, blank=True, default='')
|
||||||
email = models.EmailField(_('email address'), unique=True)
|
email = models.EmailField(_('email address'), unique=True)
|
||||||
|
|
||||||
|
objects = UserManager()
|
||||||
|
|
||||||
def get_role_permissions(self):
|
def get_role_permissions(self):
|
||||||
perms = set()
|
perms = set()
|
||||||
for role in Role.objects.get_roles_for_user(self):
|
for role in Role.objects.get_roles_for_user(self):
|
||||||
|
|
@ -70,6 +74,25 @@ class SchoolClass(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'SchoolClass {}-{}'.format(self.id, self.name)
|
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):
|
def is_user_in_schoolclass(self, user):
|
||||||
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
|
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,16 @@
|
||||||
#
|
#
|
||||||
# Created on 2019-10-01
|
# Created on 2019-10-01
|
||||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||||
import re
|
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from django.contrib.auth import authenticate, login
|
from django.contrib.auth import authenticate, login
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
|
||||||
|
from registration.models import License
|
||||||
class FieldError(graphene.ObjectType):
|
|
||||||
code = graphene.String()
|
|
||||||
|
|
||||||
|
|
||||||
class MutationError(graphene.ObjectType):
|
class LoginError(graphene.ObjectType):
|
||||||
field = graphene.String()
|
field = graphene.String()
|
||||||
errors = graphene.List(FieldError)
|
|
||||||
|
|
||||||
|
|
||||||
class Login(relay.ClientIDMutation):
|
class Login(relay.ClientIDMutation):
|
||||||
|
|
@ -29,17 +25,30 @@ class Login(relay.ClientIDMutation):
|
||||||
password_input = graphene.String()
|
password_input = graphene.String()
|
||||||
|
|
||||||
success = graphene.Boolean()
|
success = graphene.Boolean()
|
||||||
errors = graphene.List(MutationError) # todo: change for consistency
|
errors = graphene.List(LoginError) # todo: change for consistency
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
|
|
||||||
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input'))
|
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input'))
|
||||||
if user is not None:
|
if user is None:
|
||||||
login(info.context, user)
|
error = LoginError(field='invalid_credentials')
|
||||||
return cls(success=True, errors=[])
|
return cls(success=False, errors=[error])
|
||||||
else:
|
|
||||||
return cls(success=False, errors=['invalid_credentials'])
|
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:
|
class UserMutations:
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ def validate_old_new_password(value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def validate_strong_email(password):
|
def validate_strong_password(password):
|
||||||
|
|
||||||
has_number = re.search('\d', password)
|
has_number = re.search('\d', password)
|
||||||
has_upper = re.search('[A-Z]', 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)
|
new_password = CharField(allow_blank=True, min_length=MIN_PASSWORD_LENGTH)
|
||||||
|
|
||||||
def validate_new_password(self, value):
|
def validate_new_password(self, value):
|
||||||
return validate_strong_email(value)
|
return validate_strong_password(value)
|
||||||
|
|
||||||
def validate_old_password(self, value):
|
def validate_old_password(self, value):
|
||||||
return validate_old_password(value, self.context.username)
|
return validate_old_password(value, self.context.username)
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,15 @@ from graphene.test import Client
|
||||||
|
|
||||||
from api.schema_public import schema
|
from api.schema_public import schema
|
||||||
from core.factories import UserFactory
|
from core.factories import UserFactory
|
||||||
|
from registration.factories import LicenseTypeFactory, LicenseFactory
|
||||||
|
from users.models import Role
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetTests(TestCase):
|
class PasswordResetTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch')
|
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('/')
|
request = RequestFactory().post('/')
|
||||||
|
|
||||||
|
|
@ -62,3 +66,25 @@ class PasswordResetTests(TestCase):
|
||||||
|
|
||||||
result = self.make_login_mutation(self.user.email, 'test1234')
|
result = self.make_login_mutation(self.user.email, 'test1234')
|
||||||
self.assertFalse(result.get('data').get('login').get('success'))
|
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
|
from users.factories import SchoolClassFactory
|
||||||
|
|
||||||
|
|
||||||
class PasswordUpdate(TestCase):
|
class MySchoolClasses(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = UserFactory(username='aschi')
|
self.user = UserFactory(username='aschi')
|
||||||
self.another_user = UserFactory(username='pesche')
|
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