Merge branch 'develop' into feature/licensing
# Conflicts: # Pipfile.lock # client/cypress/fixtures/schema.json # client/src/graphql/client.js # client/src/graphql/resolvers.js # client/src/graphql/typedefs.js # client/src/main.js # client/src/router/index.js # server/users/models.py
This commit is contained in:
commit
d95e8ca492
1
Pipfile
1
Pipfile
|
|
@ -10,6 +10,7 @@ python_version = '3.6'
|
|||
awscli = "*"
|
||||
ipdb = "*"
|
||||
coverage = "*"
|
||||
django-silk = "*"
|
||||
|
||||
[packages]
|
||||
factory-boy = "==2.11.0"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e7807798e8b5c39d7c1bc57d61520f2c888da08d2b6061f07758e00b490fdbd6"
|
||||
"sha256": "ca4f635dc983134e4569df2af8f0d73f488211bc2a97d11c194f76279f942977"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
|
@ -48,18 +48,18 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:27e221d3868f35687807e5c920f7e8d4872f722f64196a7fd274a06ad65beec0",
|
||||
"sha256:8ff4e3d9e5d6a26dd7494afc68dc96afe6b7bda88130cca84cd58702d888ed27"
|
||||
"sha256:629ce3be236b6e0aed52358146eea9ffa7679d6cd1cc9b3e12332226270d6499",
|
||||
"sha256:b1351e62136fae29be8fcbb1c4890f1d72017d57e33051d435a8bf9f71212fde"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.11.10"
|
||||
"version": "==1.11.14"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:cf3144994191847e30ef76781af867009bdc233b3f1f4736615e5330687a891e",
|
||||
"sha256:f11ff8616f46ca04697df031e622c9ed50931b9d649d4e719f961e9d80771e8d"
|
||||
"sha256:34ad4d73e6bef5c8ad956c66354611628cdebd431fe2927261ed9c068b9cfb7a",
|
||||
"sha256:6570f2ba046956d5b548ab2346d32f713ecf8b8cb098a839d73fcf832ccfa223"
|
||||
],
|
||||
"version": "==1.14.10"
|
||||
"version": "==1.14.14"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
|
|
@ -535,10 +535,10 @@
|
|||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:2525bae2a530195576da53671bae8ca8c55ee8e33bc2225a65e804476611ea5a",
|
||||
"sha256:4924e10451cc37901945806423d16c2c2040a6530645a614ed87e995ccec764c"
|
||||
"sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
|
||||
"sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
|
||||
],
|
||||
"version": "==0.3.2"
|
||||
"version": "==0.3.3"
|
||||
},
|
||||
"sendgrid": {
|
||||
"hashes": [
|
||||
|
|
@ -603,6 +603,7 @@
|
|||
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||
],
|
||||
"markers": "python_version != '3.4'",
|
||||
"version": "==1.25.8"
|
||||
},
|
||||
"wagtail": {
|
||||
|
|
@ -652,21 +653,26 @@
|
|||
}
|
||||
},
|
||||
"develop": {
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
|
||||
"sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
|
||||
],
|
||||
"version": "==3.2.3"
|
||||
},
|
||||
"autopep8": {
|
||||
"hashes": [
|
||||
"sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"
|
||||
],
|
||||
"version": "==1.5"
|
||||
},
|
||||
"awscli": {
|
||||
"hashes": [
|
||||
<<<<<<< HEAD
|
||||
"sha256:4c49f085fb827ca1aeba5e6e5e39f6005110a0059b5c772aeb1d51c4f33c4028",
|
||||
"sha256:9459ac705c2a5d8724057492800c52084df714b624853eb3331087ecf8726a22"
|
||||
"sha256:2748af4e77728ced50d7d5bda0fa980449bd71eedff90ee643bee86ed4283d2f",
|
||||
"sha256:9118015f4bbab1c671d9c9927d07b6f7eadb7e1e8bbb2b06dc849c3de578d692"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.9"
|
||||
=======
|
||||
"sha256:7ddb43a5423725adfabb752e21ac7d47c0b440a10128e9884f578848c2369555",
|
||||
"sha256:e5617cb8244863566df1cb12564e439b224e88ea2270f27b28da82df093eba0a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.11"
|
||||
>>>>>>> Add hello page, add local mutation
|
||||
"version": "==1.17.14"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
|
|
@ -677,24 +683,32 @@
|
|||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
<<<<<<< HEAD
|
||||
"sha256:cf3144994191847e30ef76781af867009bdc233b3f1f4736615e5330687a891e",
|
||||
"sha256:f11ff8616f46ca04697df031e622c9ed50931b9d649d4e719f961e9d80771e8d"
|
||||
"sha256:34ad4d73e6bef5c8ad956c66354611628cdebd431fe2927261ed9c068b9cfb7a",
|
||||
"sha256:6570f2ba046956d5b548ab2346d32f713ecf8b8cb098a839d73fcf832ccfa223"
|
||||
],
|
||||
"version": "==1.14.10"
|
||||
=======
|
||||
"sha256:5ad6f4b80f3151fc5aa940f89fb6bf2db3064bf8d3f8919f5b60f5c741054ba5",
|
||||
"sha256:ac783a87bd90be8a4d08101bfc0d29a4b35fe0ced387f5c8bc91d01cdaa7a168"
|
||||
"version": "==1.14.14"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||
],
|
||||
"version": "==1.14.11"
|
||||
>>>>>>> Add hello page, add local mutation
|
||||
"version": "==2019.11.28"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
|
||||
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
"markers": "python_version != '3.4'",
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
|
|
@ -740,6 +754,22 @@
|
|||
],
|
||||
"version": "==4.4.1"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:48522428f4a285cf265af969f4744c5ebb027c7f41958ba48b639ace2068ffe7",
|
||||
"sha256:a794f7a2f4b7c928eecfbc4ebad03712ff27fb545abe269bf01aa8500781eb1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.15"
|
||||
},
|
||||
"django-silk": {
|
||||
"hashes": [
|
||||
"sha256:9d5d66628689230288d1020de186b86e6f38583f611b5dd796ec988bb6a6333e",
|
||||
"sha256:b0033ec3713882a5abb8b3db2b4392a746b83ce164ab7be568f0eeec4ba78a98"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
|
||||
|
|
@ -748,6 +778,19 @@
|
|||
],
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"gprof2dot": {
|
||||
"hashes": [
|
||||
"sha256:b43fe04ebb3dfe181a612bbfc69e90555b8957022ad6a466f0308ed9c7f22e99"
|
||||
],
|
||||
"version": "==2019.11.30"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"ipdb": {
|
||||
"hashes": [
|
||||
"sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"
|
||||
|
|
@ -777,6 +820,13 @@
|
|||
],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250",
|
||||
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"
|
||||
],
|
||||
"version": "==2.11.1"
|
||||
},
|
||||
"jmespath": {
|
||||
"hashes": [
|
||||
"sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6",
|
||||
|
|
@ -784,6 +834,44 @@
|
|||
],
|
||||
"version": "==0.9.4"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57",
|
||||
|
|
@ -827,6 +915,13 @@
|
|||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
|
||||
|
|
@ -841,6 +936,13 @@
|
|||
],
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||
],
|
||||
"version": "==2019.3"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||
|
|
@ -857,6 +959,14 @@
|
|||
],
|
||||
"version": "==5.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"rsa": {
|
||||
"hashes": [
|
||||
"sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5",
|
||||
|
|
@ -866,10 +976,10 @@
|
|||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:2525bae2a530195576da53671bae8ca8c55ee8e33bc2225a65e804476611ea5a",
|
||||
"sha256:4924e10451cc37901945806423d16c2c2040a6530645a614ed87e995ccec764c"
|
||||
"sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
|
||||
"sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
|
||||
],
|
||||
"version": "==0.3.2"
|
||||
"version": "==0.3.3"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
|
@ -878,6 +988,13 @@
|
|||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
|
||||
"sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
|
||||
],
|
||||
"version": "==0.3.0"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
|
||||
|
|
@ -890,6 +1007,7 @@
|
|||
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||
],
|
||||
"markers": "python_version != '3.4'",
|
||||
"version": "==1.25.8"
|
||||
},
|
||||
"wcwidth": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"me": {
|
||||
"id": "VXNlck5vZGU6NQ==",
|
||||
"pk": 5,
|
||||
"username": "rahel.cueni",
|
||||
"email": "rahel.cueni@skillbox.example",
|
||||
"firstName": "Rahel",
|
||||
"lastName": "Cueni",
|
||||
"avatarUrl": "",
|
||||
"lastModule": {
|
||||
"id": "TW9kdWxlTm9kZToxNw==",
|
||||
"slug": "lohn-und-budget",
|
||||
"__typename": "ModuleNode"
|
||||
},
|
||||
"selectedClass": {
|
||||
"id": "U2Nob29sQ2xhc3NOb2RlOjI=",
|
||||
"__typename": "SchoolClassNode"
|
||||
},
|
||||
"schoolClasses": {
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"id": "U2Nob29sQ2xhc3NOb2RlOjE=",
|
||||
"name": "FLID2018a",
|
||||
"__typename": "SchoolClassNode"
|
||||
},
|
||||
"__typename": "SchoolClassNodeEdge"
|
||||
}
|
||||
],
|
||||
"__typename": "SchoolClassNodeConnection"
|
||||
},
|
||||
"__typename": "UserNode",
|
||||
"permissions": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"me": {
|
||||
"id": "VXNlck5vZGU6NQ==",
|
||||
"pk": 5,
|
||||
"username": "hansli",
|
||||
"email": "hansli@skillbox.example",
|
||||
"firstName": "Hansli",
|
||||
"lastName": "Alleini",
|
||||
"avatarUrl": "",
|
||||
"lastModule": null,
|
||||
"selectedClass": null,
|
||||
"schoolClasses": {
|
||||
"edges": [],
|
||||
"__typename": "SchoolClassNodeConnection"
|
||||
},
|
||||
"__typename": "UserNode",
|
||||
"permissions": []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"data": {
|
||||
"__schema": {
|
||||
"queryType": {
|
||||
"name": "Query"
|
||||
|
|
@ -3125,6 +3126,18 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "expiryDate",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
@ -3350,6 +3363,18 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "hiddenContentBlocks",
|
||||
"description": null,
|
||||
|
|
@ -9385,6 +9410,33 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "joinClass",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "JoinClassInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "JoinClassPayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "addProject",
|
||||
"description": null,
|
||||
|
|
@ -10534,6 +10586,16 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "userIdInput",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": null,
|
||||
|
|
@ -11609,6 +11671,88 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "JoinClassPayload",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "success",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "schoolClass",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SchoolClassNode",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "JoinClassInput",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "code",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "clientMutationId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AddProjectPayload",
|
||||
|
|
@ -15826,5 +15970,5 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
describe('Survey', () => {
|
||||
describe('Bookmarks', () => {
|
||||
beforeEach(() => {
|
||||
// todo: mock all the graphql queries and mutations
|
||||
cy.exec("python ../server/manage.py prepare_bookmarks_for_cypress");
|
||||
|
|
@ -27,7 +27,6 @@ describe('Survey', () => {
|
|||
|
||||
cy.get('@interviewContent').within(() => {
|
||||
cy.get('.bookmark-actions__edit-note').click();
|
||||
|
||||
});
|
||||
|
||||
cy.get('[data-cy=bookmark-note]').within(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
const schema = require('../fixtures/schema.json');
|
||||
const me = require('../fixtures/me.join-class.json');
|
||||
|
||||
describe('Join Class', () => {
|
||||
beforeEach(() => {
|
||||
cy.server();
|
||||
|
||||
cy.mockGraphql({
|
||||
schema: schema,
|
||||
});
|
||||
|
||||
cy.viewport('macbook-15');
|
||||
cy.apolloLogin('rahel.cueni', 'test');
|
||||
|
||||
});
|
||||
|
||||
it('should join class', () => {
|
||||
cy.mockGraphqlOps({
|
||||
operations: {
|
||||
MeQuery: me,
|
||||
JoinClass: {
|
||||
joinClass: {
|
||||
success: true,
|
||||
schoolClass: {
|
||||
id: "U2Nob29sQ2xhc3NOb2RlOjI=",
|
||||
name: "KF1A",
|
||||
__typename: "SchoolClassNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.get('[data-cy=user-widget-avatar]').click();
|
||||
cy.get('[data-cy=class-selection]').click();
|
||||
cy.get('[data-cy=class-selection-entry]').should('have.length', 1);
|
||||
|
||||
cy.get('[data-cy=join-class-link]').click();
|
||||
|
||||
cy.get('[data-cy=input-class-code]').type('XXXX');
|
||||
cy.get('[data-cy=join-class]').click();
|
||||
|
||||
cy.get('[data-cy=class-selection]').click();
|
||||
cy.get('[data-cy=class-selection-entry]').should('have.length', 2);
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
const schema = require('../fixtures/schema.json');
|
||||
const me = require('../fixtures/me.new-student.json');
|
||||
|
||||
describe('New student', () => {
|
||||
it('shows "Enter Code" page and adds the user to a class', () => {
|
||||
|
||||
cy.server();
|
||||
|
||||
cy.mockGraphql({
|
||||
schema: schema,
|
||||
});
|
||||
|
||||
|
||||
cy.apolloLogin('hansli', 'test');
|
||||
|
||||
cy.mockGraphqlOps({
|
||||
operations: {
|
||||
MeQuery: me,
|
||||
JoinClass: {
|
||||
joinClass: {
|
||||
success: true,
|
||||
schoolClass: {
|
||||
id: "U2Nob29sQ2xhc3NOb2RlOjI=",
|
||||
name: "KF1A",
|
||||
__typename: "SchoolClassNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cy.visit('/');
|
||||
cy.get('[data-cy=join-class-title]').should('contain', 'Zugangscode');
|
||||
cy.get('[data-cy=input-class-code]').type('XXXX');
|
||||
cy.get('[data-cy=join-class]').click();
|
||||
cy.get('[data-cy=class-list-title]').should('contain', 'Klassenliste');
|
||||
});
|
||||
});
|
||||
|
|
@ -10,7 +10,6 @@ describe('Survey', () => {
|
|||
});
|
||||
|
||||
cy.viewport('macbook-15');
|
||||
|
||||
cy.apolloLogin('rahel.cueni', 'test');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -59,15 +59,8 @@
|
|||
display: flex;
|
||||
|
||||
&__link {
|
||||
font-size: 1.0625rem;
|
||||
padding: 0 24px;
|
||||
font-family: $sans-serif-font-family;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $color-silver-dark;
|
||||
|
||||
&--active {
|
||||
color: $color-brand;
|
||||
}
|
||||
@include default-link;
|
||||
}
|
||||
|
||||
$parent: &;
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<header class="header-bar">
|
||||
<top-navigation></top-navigation>
|
||||
<content-navigation></content-navigation>
|
||||
<router-link to="/" class="header-bar__logo" data-cy="home-link">
|
||||
<logo></logo>
|
||||
</router-link>
|
||||
<div class="user-header">
|
||||
<class-selection-widget />
|
||||
<a class="user-header__sidebar-link" @click="openSidebar()"><current-class class="user-header__current-class"/></a>
|
||||
|
||||
<user-widget v-bind="me"></user-widget>
|
||||
</div>
|
||||
<book-navigation v-if="showSubnavigation">
|
||||
|
|
@ -14,23 +15,26 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import TopNavigation from '@/components/TopNavigation.vue';
|
||||
import ContentNavigation from '@/components/ContentNavigation.vue';
|
||||
import BookNavigation from '@/components/book-navigation/BookNavigation';
|
||||
import UserWidget from '@/components/UserWidget.vue';
|
||||
import LogoutWidget from '@/components/LogoutWidget.vue';
|
||||
import Logo from '@/components/icons/Logo';
|
||||
import ClassSelectionWidget from '@/components/ClassSelectionWidget';
|
||||
import CurrentClass from '@/components/school-class/CurrentClass';
|
||||
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
import openSidebar from '@/mixins/open-sidebar';
|
||||
import me from '@/mixins/me';
|
||||
|
||||
export default {
|
||||
mixins: [openSidebar, me],
|
||||
|
||||
components: {
|
||||
TopNavigation,
|
||||
ContentNavigation,
|
||||
UserWidget,
|
||||
LogoutWidget,
|
||||
BookNavigation,
|
||||
Logo,
|
||||
ClassSelectionWidget
|
||||
CurrentClass
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -38,18 +42,6 @@
|
|||
return this.$route.meta.subnavigation;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
me: {}
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
me: {
|
||||
query: ME_QUERY,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -127,5 +119,13 @@
|
|||
|
||||
.user-header {
|
||||
display: flex;
|
||||
|
||||
&__current-class {
|
||||
margin-right: $large-spacing;
|
||||
}
|
||||
|
||||
&__sidebar-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,17 @@
|
|||
<template>
|
||||
<div class="user-widget" :class="{'user-widget--is-profile': isProfile}">
|
||||
<div class="user-widget__avatar" @click="toggleShowPopover()">
|
||||
<div class="user-widget__avatar" data-cy="user-widget-avatar" @click="openSidebar()">
|
||||
<avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile"/>
|
||||
</div>
|
||||
<widget-popover v-if="showPopover && showMenu"
|
||||
@hide-me="showPopover = false"
|
||||
:mobile="mobile"
|
||||
class="user-widget__popover ">
|
||||
<li class="popover-links__link popover-links__link--large popover-links__link--emph">{{firstName}} {{lastName}}
|
||||
</li>
|
||||
<li class="popover-links__link popover-links__link--large">
|
||||
<router-link to="/me/activity" @click="toggleShowPopover()">Aktivität</router-link>
|
||||
</li>
|
||||
<li class="popover-links__link popover-links__link--large" @click="toggleShowPopover()">
|
||||
<router-link to="/me/profile">Profil</router-link>
|
||||
</li>
|
||||
<li class="popover-links__link popover-links__link--large" @click="toggleShowPopover()">
|
||||
<router-link to="/me/myclasses">Klassenliste</router-link>
|
||||
</li>
|
||||
<li class="popover-links__link popover-links__link--large" data-cy="logout" @click="logout()">
|
||||
<a>Logout</a>
|
||||
</li>
|
||||
</widget-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
|
||||
import Avatar from '@/components/profile/Avatar';
|
||||
import WidgetPopover from '@/components/WidgetPopover';
|
||||
import openSidebar from '@/mixins/open-sidebar';
|
||||
|
||||
export default {
|
||||
// todo: clean up unneeded props
|
||||
props: {
|
||||
firstName: {
|
||||
type: String
|
||||
|
|
@ -51,31 +32,10 @@
|
|||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showPopover: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleShowPopover() {
|
||||
if (this.showMenu) {
|
||||
this.showPopover = !this.showPopover;
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.$apollo.mutate({
|
||||
mutation: LOGOUT_MUTATION,
|
||||
}).then(({data}) => {
|
||||
if (data.logout.success) {
|
||||
location.replace('/logout')
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mixins: [openSidebar],
|
||||
|
||||
components: {
|
||||
Avatar, WidgetPopover
|
||||
Avatar
|
||||
},
|
||||
computed: {
|
||||
isProfile() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="mobile-navigation">
|
||||
<top-navigation class="mobile-navigation__main" :mobile="true"></top-navigation>
|
||||
<content-navigation class="mobile-navigation__main" :mobile="true"></content-navigation>
|
||||
<div class="mobile-navigation__close-button" @click="hideMobileNavigation">
|
||||
<cross class="mobile-navigation__close-icon"></cross>
|
||||
</div>
|
||||
|
|
@ -16,14 +16,14 @@
|
|||
import Cross from '@/components/icons/Cross';
|
||||
import UserWidget from '@/components/UserWidget';
|
||||
import LogoutWidget from '@/components/LogoutWidget';
|
||||
import TopNavigation from '@/components/TopNavigation';
|
||||
import ClassSelectionWidget from '@/components/ClassSelectionWidget';
|
||||
import ContentNavigation from '@/components/ContentNavigation';
|
||||
import ClassSelectionWidget from '@/components/school-class/ClassSelectionWidget';
|
||||
|
||||
import {meQuery} from '@/graphql/queries';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TopNavigation,
|
||||
ContentNavigation,
|
||||
Cross,
|
||||
UserWidget,
|
||||
LogoutWidget,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 400 400">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<circle class="cls-1" cx="200" cy="200" r="197"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="bg">
|
||||
<circle class="cls-2" cx="200" cy="200" r="197"/>
|
||||
</g>
|
||||
<g id="objects">
|
||||
<g class="cls-3">
|
||||
<path class="cls-4"
|
||||
d="M380.03229,346.82485l-99.89952-29.68554-75.91247-7.58045h-2.40707l-78.31955,7.58045-103.526,30.44984s75.62737,95.55272,183.04906,95.55272S380.03229,346.82485,380.03229,346.82485Z"/>
|
||||
<path class="cls-5"
|
||||
d="M272.15765,65.53589a86.01591,86.01591,0,0,0-20.524-14.33295,103.61294,103.61294,0,0,0-49.8204-11.92284,103.61271,103.61271,0,0,0-49.8198,11.92255A86.01706,86.01706,0,0,0,131.4688,65.53589c-47.89186,45.957-59.684,136.053-53.17653,195.51559,22.30221,28.20465,70.43168,35.98046,103.97675,29.59277-1.699-28.98109-18.28465-55.36789-18.09824-84.40807,16.82179-9.95623,9.42329-59.11938,10.702-81.59916l26.94039-6.97556,26.9404,6.97556c1.27875,22.47978-6.11974,71.64293,10.70211,81.59916.18642,29.04018-16.39928,55.427-18.09824,84.40807,33.545,6.38769,81.67447-1.38812,103.97668-29.59277C331.84167,201.58886,320.04951,111.49293,272.15765,65.53589Z"/>
|
||||
<path class="cls-6"
|
||||
d="M237.057,266.23162H166.56944c0,34.9025-27.34,45.73431-43.07576,50.90769,15.73577,67.03868,142.10688,67.03868,156.63909,0C260.78642,311.96593,241.52987,305.94826,237.057,266.23162Z"/>
|
||||
<ellipse class="cls-7" cx="202.35227" cy="179.47219" rx="79.23803" ry="97.02616"/>
|
||||
<path class="cls-8"
|
||||
d="M117.61519,154.166c10.8764-3.25836,22.30778,5.29762,25.51206,19.09451,3.20424,13.7965-3.02464,27.64251-13.90108,30.90087-10.87644,3.25678-22.30919-5.29877-25.51339-19.09389-3.20425-13.79708,3.026-27.64273,13.90241-30.90149Z"/>
|
||||
<path class="cls-5"
|
||||
d="M158.77973,88.35379c34.35007,53.44875,83.71361,66.59188,126.44529,82.83956C322.20435,145.19484,244.7734,11.96688,158.77973,88.35379Z"/>
|
||||
<path class="cls-5"
|
||||
d="M181.50651,94.88021c-10.83147,33.057-30.31262,48.8307-55.51284,58.59347C109.4863,115.38906,134.25941,72.625,181.50651,94.88021Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d1eee7;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
clip-path: url(#clip-path);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #17a887;
|
||||
}
|
||||
|
||||
.cls-4, .cls-5, .cls-6, .cls-8 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #102d24;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #e5f5f2;
|
||||
}
|
||||
|
||||
.cls-7, .cls-8 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,28 +1,49 @@
|
|||
<template>
|
||||
<div class="avatar">
|
||||
<transition name="fade">
|
||||
<user-icon v-show="!isAvatarLoaded" class="avatar__placeholder" :class="{'avatar__placeholder--highlighted': iconHighlighted}"></user-icon>
|
||||
<default-avatar v-show="!isAvatarLoaded" class="avatar__placeholder"
|
||||
:class="{'avatar__placeholder--highlighted': iconHighlighted}"></default-avatar>
|
||||
</transition>
|
||||
<transition name="show">
|
||||
<div v-show="isAvatarLoaded" class="avatar__image" ref="avatarImage" :style="{'background-image': `url(${this.avatarUrl})`}"></div>
|
||||
<div v-show="isAvatarLoaded" class="avatar__image" ref="avatarImage"
|
||||
:style="{'background-image': `url(${this.avatarUrl})`}"></div>
|
||||
</transition>
|
||||
<img class="avatar__fake-image" :src="avatarUrl" ref="fakeImage"/>
|
||||
|
||||
<div class="avatar__edit" v-if="editable" @click="closeSidebar">
|
||||
<router-link :to="{name: 'profile'}">
|
||||
<pen-icon></pen-icon>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DefaultAvatar from '@/components/icons/DefaultAvatar';
|
||||
import PenIcon from '@/components/icons/PenIcon';
|
||||
|
||||
import UserIcon from '@/components/icons/UserIcon';
|
||||
import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql';
|
||||
|
||||
export default {
|
||||
props: ['avatarUrl', 'iconHighlighted'],
|
||||
components: {UserIcon},
|
||||
data () {
|
||||
props: {
|
||||
avatarUrl: {
|
||||
type: String
|
||||
},
|
||||
iconHighlighted: {},
|
||||
editable: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DefaultAvatar,
|
||||
PenIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAvatarLoaded: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
if (this.avatarUrl !== '') {
|
||||
this.$refs.fakeImage.addEventListener('load', () => {
|
||||
if (this.$refs.fakeImage) {
|
||||
|
|
@ -30,7 +51,17 @@
|
|||
this.isAvatarLoaded = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeSidebar() {
|
||||
this.$apollo.mutate({
|
||||
mutation: TOGGLE_SIDEBAR,
|
||||
variables: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -45,11 +76,11 @@
|
|||
width: $max-width;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&__placeholder {
|
||||
height: $max-width;
|
||||
fill: $color-silver-dark;
|
||||
border-radius: 50%;
|
||||
|
||||
&--highlighted {
|
||||
fill: $color-brand;
|
||||
|
|
@ -57,14 +88,14 @@
|
|||
}
|
||||
|
||||
&__image {
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
|
||||
&--landscape {
|
||||
&--landscape {
|
||||
width: auto;
|
||||
height: $max-width;
|
||||
}
|
||||
|
|
@ -75,10 +106,25 @@
|
|||
height: 0;
|
||||
}
|
||||
|
||||
.fade-leave-active, .show-enter-active {
|
||||
&__edit {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: block;
|
||||
left: 50%;
|
||||
bottom: -7px;
|
||||
transform: translateX(50%);
|
||||
background-color: $color-white;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.fade-leave-active, .show-enter-active {
|
||||
transition: opacity .5s;
|
||||
}
|
||||
.fade-leave-to, .show-enter {
|
||||
.fade-leave-to, .show-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<!-- Not currently in use, but keeping the file in case it's needed again -->
|
||||
<div class="password-reset">
|
||||
<h2 class="password-reset__header">Passwort ändern</h2>
|
||||
<div v-if="showSuccess" class="success-message">
|
||||
|
|
@ -7,7 +8,7 @@
|
|||
<password-change-form
|
||||
@passwordSubmited="resetPassword"
|
||||
:oldPasswordErrors="oldPasswordErrors"
|
||||
:newPasswordErrors="newPasswordErrors" />
|
||||
:newPasswordErrors="newPasswordErrors"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
resetPassword (passwords) {
|
||||
resetPassword(passwords) {
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_PASSWORD_MUTATION,
|
||||
variables: {
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
passwordInput: passwords
|
||||
}
|
||||
}
|
||||
}).then(({ data }) => {
|
||||
}).then(({data}) => {
|
||||
if (data.updatePassword.success) {
|
||||
this.oldPasswordErrors = [];
|
||||
this.newPasswordErrors = [];
|
||||
|
|
@ -56,14 +57,14 @@
|
|||
console.log('fail', error)
|
||||
});
|
||||
},
|
||||
handleOldPasswordError (error) {
|
||||
handleOldPasswordError(error) {
|
||||
this.oldPasswordErrors = error.errors.map((fieldError) => {
|
||||
if (fieldError.code === 'invalid') {
|
||||
return 'Die Eingabe ist falsch'
|
||||
}
|
||||
});
|
||||
},
|
||||
handleNewPasswordError (error) {
|
||||
handleNewPasswordError(error) {
|
||||
this.newPasswordErrors = error.errors.map((fieldError) => {
|
||||
if (fieldError.code === 'invalid') {
|
||||
return 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten'
|
||||
|
|
|
|||
|
|
@ -11,22 +11,18 @@
|
|||
</a>
|
||||
</div>
|
||||
<avatar-upload-form v-else @avatarUpdate="updateAvatar"/>
|
||||
<password-change />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import UPDATE_AVATAR_QUERY from '@/graphql/gql/mutations/updateAvatarUrl.gql';
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
import PasswordChange from '@/components/profile/PasswordChange';
|
||||
import AvatarUploadForm from '@/components/profile/AvatarUploadForm';
|
||||
import Avatar from '@/components/profile/Avatar';
|
||||
import TrashIcon from '@/components/icons/TrashIcon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PasswordChange,
|
||||
AvatarUploadForm,
|
||||
Avatar,
|
||||
TrashIcon
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="profile-sidebar" v-if="sidebar.open">
|
||||
<a class="profile-sidebar__close-link" @click="closeSidebar">
|
||||
<cross class="profile-sidebar__close-icon"></cross>
|
||||
</a>
|
||||
<profile-widget class="profile-sidebar__item"></profile-widget>
|
||||
<div class="profile-sidebar__item" @click="closeSidebar">
|
||||
<router-link to="/me/activity" class="profile-sidebar__link">Meine Aktivitäten</router-link>
|
||||
</div>
|
||||
<div class="profile-sidebar__item">
|
||||
<h3 class="profile-sidebar__subtitle">Klasse</h3>
|
||||
<class-selection-widget></class-selection-widget>
|
||||
<router-link :to="{name: 'my-classes'}" class="profile-sidebar__link">Klassenliste anzeigen</router-link>
|
||||
</div>
|
||||
<div class="profile-sidebar__item" @click="closeSidebar">
|
||||
<router-link :to="{name:'join-class'}" class="profile-sidebar__link">Zugangscode eingeben</router-link>
|
||||
</div>
|
||||
<div class="profile-sidebar__item" @click="logout">
|
||||
<a class="profile-sidebar__link">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProfileWidget from '@/components/profile/ProfileWidget';
|
||||
import Cross from '@/components/icons/Cross';
|
||||
|
||||
import SIDEBAR from '@/graphql/gql/local/sidebar.gql';
|
||||
import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql';
|
||||
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
|
||||
import ClassSelectionWidget from '@/components/school-class/ClassSelectionWidget';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClassSelectionWidget,
|
||||
ProfileWidget,
|
||||
Cross
|
||||
},
|
||||
|
||||
methods: {
|
||||
closeSidebar() {
|
||||
this.$apollo.mutate({
|
||||
mutation: TOGGLE_SIDEBAR,
|
||||
variables: {
|
||||
open: false
|
||||
}
|
||||
});
|
||||
},
|
||||
logout() {
|
||||
this.$apollo.mutate({
|
||||
mutation: LOGOUT_MUTATION,
|
||||
}).then(({data}) => {
|
||||
if (data.logout.success) {
|
||||
location.replace('/logout')
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
sidebar: {
|
||||
query: SIDEBAR
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
sidebar: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.profile-sidebar {
|
||||
padding: $large-spacing 0;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 333px;
|
||||
background-color: $color-white;
|
||||
z-index: 10;
|
||||
box-shadow: 0 3px 9px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__item {
|
||||
border-bottom: 1px solid $color-silver-light;
|
||||
padding: $large-spacing $medium-spacing;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include small-text;
|
||||
margin: 0;
|
||||
margin-bottom: $small-spacing;
|
||||
}
|
||||
|
||||
&__link {
|
||||
@include default-link;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__close-link {
|
||||
position: absolute;
|
||||
right: $small-spacing;
|
||||
top: $small-spacing;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__close-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="profile-widget">
|
||||
<div class="profile-widget__avatar">
|
||||
<avatar :avatar-url="me.avatarUrl" :editable="true"></avatar>
|
||||
</div>
|
||||
<h3 class="profile-widget__name">{{me.firstName}} {{me.lastName}}</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from '@/components/profile/Avatar';
|
||||
import {meQuery} from '@/graphql/queries';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar
|
||||
},
|
||||
|
||||
apollo: {
|
||||
me: meQuery
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
me: {}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.profile-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__name {
|
||||
@include heading-3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: $medium-spacing;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,31 +1,37 @@
|
|||
<template>
|
||||
<div class="class-selection" v-if="isTeacher">
|
||||
<div class="class-selection__selected-class selected-class" @click="showPopover = !showPopover">
|
||||
<p class="selected-class__text">Klasse: {{currentClassSelection.name}}</p>
|
||||
<div class="class-selection" v-if="currentClassSelection">
|
||||
<div data-cy="class-selection" class="class-selection__selected-class selected-class"
|
||||
@click="showPopover = !showPopover">
|
||||
<current-class class="selected-class__text"></current-class> <chevron-down class="selected-class__dropdown-icon"></chevron-down>
|
||||
</div>
|
||||
<widget-popover v-if="showPopover"
|
||||
@hide-me="showPopover = false"
|
||||
:mobile="mobile"
|
||||
class="class-selection__popover">
|
||||
<li class="popover-links__link popover-links__link--large" v-for="schoolClass in schoolClasses"
|
||||
<widget-popover v-if="showPopover"
|
||||
@hide-me="showPopover = false"
|
||||
:mobile="mobile"
|
||||
class="class-selection__popover">
|
||||
<li data-cy="class-selection-entry" class="popover-links__link popover-links__link--large"
|
||||
v-for="schoolClass in me.schoolClasses"
|
||||
:key="schoolClass.id"
|
||||
:label="schoolClass.name"
|
||||
:item="schoolClass"
|
||||
@click="updateFilter(schoolClass)">
|
||||
{{schoolClass.name}}
|
||||
</li>
|
||||
{{schoolClass.name}}
|
||||
</li>
|
||||
</widget-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetPopover from '@/components/WidgetPopover';
|
||||
import ChevronDown from '@/components/icons/ChevronDown';
|
||||
import CurrentClass from '@/components/school-class/CurrentClass';
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WidgetPopover
|
||||
WidgetPopover,
|
||||
ChevronDown,
|
||||
CurrentClass
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -37,7 +43,13 @@
|
|||
|
||||
apollo: {
|
||||
me: {
|
||||
query: ME_QUERY
|
||||
query: ME_QUERY,
|
||||
manual: true,
|
||||
result({data, loading, networkStatus}) {
|
||||
if (!loading) {
|
||||
this.me = this.$getRidOfEdges(data).me;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -47,7 +59,8 @@
|
|||
selectedClass: {
|
||||
id: ''
|
||||
},
|
||||
permissions: []
|
||||
permissions: [],
|
||||
schoolClasses: []
|
||||
},
|
||||
showPopover: false
|
||||
}
|
||||
|
|
@ -64,7 +77,7 @@
|
|||
},
|
||||
update(store, data) {
|
||||
let meData = store.readQuery({query: ME_QUERY});
|
||||
meData.me.selectedClass = selectedClass
|
||||
meData.me.selectedClass = selectedClass;
|
||||
store.writeQuery({query: ME_QUERY, data: meData});
|
||||
}
|
||||
}).catch((error) => {
|
||||
|
|
@ -76,16 +89,10 @@
|
|||
|
||||
computed: {
|
||||
currentClassSelection() {
|
||||
let currentClass = this.schoolClasses.find(schoolClass => {
|
||||
let currentClass = this.me.schoolClasses.find(schoolClass => {
|
||||
return schoolClass.id === this.me.selectedClass.id
|
||||
})
|
||||
return currentClass || this.schoolClasses[0];
|
||||
},
|
||||
schoolClasses() {
|
||||
return this.$getRidOfEdges(this.me.schoolClasses);
|
||||
},
|
||||
isTeacher() {
|
||||
return this.me.permissions.includes('users.can_manage_school_class_content');
|
||||
});
|
||||
return currentClass || this.me.schoolClasses[0];
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -97,25 +104,42 @@
|
|||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.class-selection {
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-right: $large-spacing;
|
||||
margin-bottom: $medium-spacing;
|
||||
border: 1px solid $color-silver;
|
||||
border-radius: 4px;
|
||||
padding: $small-spacing $medium-spacing;
|
||||
/*justify-self: space-between;*/
|
||||
|
||||
&__popover {
|
||||
white-space: nowrap;
|
||||
top: 40px;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
transform: translateY($small-spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.selected-class {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&__text {
|
||||
line-height: $large-spacing;
|
||||
@include regular-text;
|
||||
color: $color-silver-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: $color-brand;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-links__link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<span class="current-class">Klasse: {{currentClassName}}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import me from '@/mixins/me';
|
||||
|
||||
export default {
|
||||
mixins: [me],
|
||||
|
||||
computed: {
|
||||
currentClassName() {
|
||||
let currentClass = this.me.schoolClasses.find(schoolClass => {
|
||||
return schoolClass.id === this.me.selectedClass.id
|
||||
});
|
||||
return currentClass ? currentClass.name : this.me.schoolClasses.length ? this.me.schoolClasses[0].name : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.current-class {
|
||||
line-height: $large-spacing;
|
||||
@include regular-text;
|
||||
color: $color-silver-dark;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -111,6 +111,10 @@ export default function (uri, networkErrorCallback) {
|
|||
helloEmail: {
|
||||
__typename: 'HelloEmail',
|
||||
email: ''
|
||||
},
|
||||
sidebar: {
|
||||
__typename: 'Sidebar',
|
||||
open: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
mutation($open: Boolean!) {
|
||||
toggleSidebar(open: $open) @client
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
query Sidebar {
|
||||
sidebar @client {
|
||||
open
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
mutation JoinClass($input: JoinClassInput!) {
|
||||
joinClass(input: $input) {
|
||||
success
|
||||
schoolClass {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql';
|
||||
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
|
||||
import SIDEBAR from '@/graphql/gql/local/sidebar.gql';
|
||||
|
||||
export const resolvers = {
|
||||
Mutation: {
|
||||
|
|
@ -15,5 +16,11 @@ export const resolvers = {
|
|||
cache.writeQuery({query: HELLO_EMAIL, data});
|
||||
return data.helloEmail;
|
||||
},
|
||||
toggleSidebar: (_, {open}, {cache}) => {
|
||||
const data = cache.readQuery({query: SIDEBAR});
|
||||
data.sidebar.open = open;
|
||||
cache.writeQuery({query: SIDEBAR, data});
|
||||
return data.sidebar;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export const typeDefs = gql`
|
|||
email: String!
|
||||
}
|
||||
|
||||
|
||||
type Sidebar {
|
||||
open: Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
scrollTo(scrollTo: String!): ScrollPosition
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="blank-layout">
|
||||
<profile-sidebar></profile-sidebar>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -14,9 +15,9 @@
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import ProfileSidebar from '@/components/profile/ProfileSidebar';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
components: {ProfileSidebar},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="container skillbox" :class="specialContainerClass">
|
||||
<profile-sidebar></profile-sidebar>
|
||||
<header-bar class="header skillbox__header">
|
||||
</header-bar>
|
||||
|
||||
|
|
@ -13,11 +14,13 @@
|
|||
<script>
|
||||
import HeaderBar from '@/components/HeaderBar';
|
||||
import MobileHeader from '@/components/MobileHeader';
|
||||
import ProfileSidebar from '@/components/profile/ProfileSidebar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HeaderBar,
|
||||
MobileHeader
|
||||
MobileHeader,
|
||||
ProfileSidebar
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -170,10 +170,9 @@ router.beforeEach(async (to, from, next) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// handle users students without class
|
||||
if ((to.name !== 'noClass' && to.name !== 'licenseActivation') && loginRequired(to) && await redirectStudentsWithoutClass()) {
|
||||
next({name: 'noClass'})
|
||||
return;
|
||||
if ((to.name !== 'join-class' && to.name !== 'licenseActivation') && loginRequired(to) && await redirectStudentsWithoutClass()) {
|
||||
next({name: 'join-class'})
|
||||
return
|
||||
}
|
||||
|
||||
next();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
me: {
|
||||
selectedClass: {
|
||||
id: ''
|
||||
},
|
||||
permissions: [],
|
||||
schoolClasses: []
|
||||
},
|
||||
showPopover: false
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
me: {
|
||||
query: ME_QUERY,
|
||||
manual: true,
|
||||
result({data, loading, networkStatus}) {
|
||||
if (!loading) {
|
||||
this.me = this.$getRidOfEdges(data).me;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
openSidebar() {
|
||||
this.$apollo.mutate({
|
||||
mutation: TOGGLE_SIDEBAR,
|
||||
variables: {
|
||||
open: true
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 data-cy="join-class-title">Zugangscode eingeben</h1>
|
||||
<div>
|
||||
<div class="skillboxform-input">
|
||||
<label for="join-code" class="skillboxform-input__label">Zugangscode</label>
|
||||
<input
|
||||
id="join-code"
|
||||
class="skillbox-input skillboxform-input__input"
|
||||
:class="{'skillboxform-input__input--error': error}"
|
||||
data-cy="input-class-code"
|
||||
:value="code"
|
||||
@input="updateCode">
|
||||
<small
|
||||
v-if="error"
|
||||
class="skillboxform-input__error"
|
||||
data-cy="email-local-errors"
|
||||
>{{ error }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a class="button button--primary button--big" data-cy="join-class" @click="joinClass(code)">Klasse beitreten</a>
|
||||
<a class="button button--big" data-cy="join-class-cancel" @click="cancel">Abbrechen</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JOIN_CLASS_MUTATION from '@/graphql/gql/mutations/joinClass.gql';
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
code: '',
|
||||
error: ''
|
||||
}),
|
||||
|
||||
methods: {
|
||||
updateCode(event) {
|
||||
this.code = event.target.value;
|
||||
this.error = '';
|
||||
},
|
||||
joinClass(code) {
|
||||
this.$apollo.mutate({
|
||||
mutation: JOIN_CLASS_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
code
|
||||
}
|
||||
},
|
||||
update(store, {data: {joinClass: {schoolClass}}}) {
|
||||
if (schoolClass) {
|
||||
const data = store.readQuery({query: ME_QUERY});
|
||||
if (data) {
|
||||
data.me.schoolClasses.edges.push({
|
||||
node: schoolClass,
|
||||
__typename: 'SchoolClassNode'
|
||||
});
|
||||
store.writeQuery({query: ME_QUERY, data});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push({name: 'my-classes'});
|
||||
})
|
||||
.catch(e => {
|
||||
console.debug(e);
|
||||
if (e.message.indexOf('[CAJ]') > -1) {
|
||||
this.error = 'Sie sind dieser Klasse bereits beigetreten.';
|
||||
} else {
|
||||
this.error = 'Dieser Zugangscode ist nicht gültig.';
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.$router.go(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<script>
|
||||
import MODULE_ROOM_ENTRIES_QUERY from '@/graphql/gql/moduleRoomEntryQuery.gql';
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
import roomMixin from '@/components/mixins/room'
|
||||
import roomMixin from '@/mixins/room'
|
||||
|
||||
export default {
|
||||
props: ['slug'],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="myclasses">
|
||||
<h1 class="myclasses__header">Klassenliste</h1>
|
||||
<h1 class="myclasses__header" data-cy="class-list-title">Klassenliste</h1>
|
||||
<classlist v-for="schoolClass in schoolClasses" v-bind="schoolClass" :key="schoolClass.name" class="myclasses__class"></classlist>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@
|
|||
<div class="profile">
|
||||
<nav class="top-navigation profile-submenu profile__submenu">
|
||||
<router-link to="/me/activity" active-class="top-navigation__link--active"
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Aktivität
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Aktivität
|
||||
</router-link>
|
||||
<router-link to="/me/myclasses" active-class="top-navigation__link--active"
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Klassenliste
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Klassenliste
|
||||
</router-link>
|
||||
<router-link to="/me/profile" active-class="top-navigation__link--active"
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Profil
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Profil
|
||||
</router-link>
|
||||
<router-link :to="{name:'join-class'}" active-class="top-navigation__link--active" data-cy="join-class-link"
|
||||
class="top-navigation__link profile-submenu__item submenu-item">Klasse beitreten
|
||||
</router-link>
|
||||
</nav>
|
||||
<router-view></router-view>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
<script>
|
||||
import ROOM_ENTRIES_QUERY from '@/graphql/gql/roomEntriesQuery.gql';
|
||||
import roomMixin from '@/components/mixins/room'
|
||||
import roomMixin from '@/mixins/room'
|
||||
|
||||
export default {
|
||||
props: ['slug'],
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@
|
|||
<div class="start-page__news news">
|
||||
<h2 class="news__title">News</h2>
|
||||
<news-teaser date="20. Dezember 2019" title="Blockchain"
|
||||
url="http://abunews-blockchain.webflow.io/"></news-teaser>
|
||||
url="https://myskillbox-abu-news.webflow.io/blockchain"></news-teaser>
|
||||
<news-teaser date="9. September 2019" title="Klima – was sonst?"
|
||||
url="https://abunews-1f178193a10edaabf-4caff67b27d10.webflow.io/"></news-teaser>
|
||||
url="https://myskillbox-abu-news.webflow.io/klima"></news-teaser>
|
||||
<!--<news-teaser date="31. Oktober 2018" title="Sommerzeit - Festivalzeit"-->
|
||||
<!--url="https://abunews.webflow.io/"></news-teaser>-->
|
||||
<div class="news__more">Mehr...</div>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import checkEmail from '@/pages/check-email'
|
|||
import emailVerification from '@/pages/email-verification'
|
||||
import licenseActivation from '@/pages/license-activation'
|
||||
import forgotPassword from '@/pages/forgot-password'
|
||||
import joinClass from '@/pages/joinClass'
|
||||
|
||||
import store from '@/store/index';
|
||||
|
||||
|
|
@ -136,6 +137,7 @@ const routes = [
|
|||
{path: '', name: 'profile-activity', component: activity, meta: {isProfile: true}},
|
||||
]
|
||||
},
|
||||
{path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'simple'}},
|
||||
{
|
||||
path: '/survey/:id',
|
||||
component: surveyPage,
|
||||
|
|
|
|||
|
|
@ -141,6 +141,17 @@
|
|||
font-size: toRem(18px);
|
||||
}
|
||||
|
||||
@mixin default-link {
|
||||
font-size: toRem(18px);
|
||||
font-family: $sans-serif-font-family;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $color-silver-dark;
|
||||
|
||||
&--active {
|
||||
color: $color-brand;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin page-form-input-heading {
|
||||
display: block;
|
||||
margin-bottom: $medium-spacing;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import graphene
|
||||
from django.db.models import Q
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
|
@ -85,24 +86,20 @@ class ChapterNode(DjangoObjectType):
|
|||
def resolve_content_blocks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
school_classes = user.school_classes.values_list('pk')
|
||||
by_parent = ContentBlock.get_by_parent(self).prefetch_related(
|
||||
'visible_for').prefetch_related(
|
||||
'hidden_for')
|
||||
|
||||
by_parent = ContentBlock.get_by_parent(self) \
|
||||
.prefetch_related('visible_for') \
|
||||
.prefetch_related('hidden_for')
|
||||
|
||||
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
|
||||
default_blocks = Q(user_created=False)
|
||||
owned_by_user = Q(user_created=True, owner=user)
|
||||
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__in=school_classes))
|
||||
|
||||
if user.has_perm('users.can_manage_school_class_content'): # teacher
|
||||
publisher_content_blocks = by_parent.filter(user_created=False)
|
||||
user_created_content_blocks = by_parent.filter(user_created=True, owner=user)
|
||||
return by_parent.filter(default_blocks | owned_by_user)
|
||||
else: # student
|
||||
publisher_content_blocks = by_parent.filter(user_created=False).exclude(
|
||||
hidden_for__in=school_classes)
|
||||
|
||||
self_created_content_blocks = by_parent.filter(user_created=True, owner=user)
|
||||
|
||||
user_created_content_blocks = by_parent.filter(user_created=True,
|
||||
visible_for__in=school_classes).union(
|
||||
self_created_content_blocks)
|
||||
|
||||
return publisher_content_blocks.union(user_created_content_blocks)
|
||||
return by_parent.filter(default_blocks | teacher_created_and_visible)
|
||||
|
||||
def resolve_bookmark(self, info, **kwags):
|
||||
return ChapterBookmark.objects.filter(
|
||||
|
|
@ -234,24 +231,6 @@ class BookNode(DjangoObjectType):
|
|||
return Topic.get_by_parent(self)
|
||||
|
||||
|
||||
# todo: do we need this?
|
||||
# class FilteredChapterNode(DjangoObjectType):
|
||||
# content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
||||
#
|
||||
# class Meta:
|
||||
# model = Chapter
|
||||
# only_fields = [
|
||||
# 'slug', 'title', 'description',
|
||||
# ]
|
||||
# filter_fields = [
|
||||
# 'slug', 'title',
|
||||
# ]
|
||||
# interfaces = (relay.Node,)
|
||||
#
|
||||
# def resolve_content_blocks(self, *args, **kwargs):
|
||||
# return ContentBlock.get_by_parent(self)
|
||||
|
||||
|
||||
class BookQuery(object):
|
||||
book = relay.Node.Field(BookNode)
|
||||
topic = graphene.Field(TopicNode, slug=graphene.String())
|
||||
|
|
@ -287,7 +266,6 @@ class BookQuery(object):
|
|||
elif slug is not None:
|
||||
module = Module.objects.get(slug=slug)
|
||||
|
||||
|
||||
return module
|
||||
|
||||
def resolve_topic(self, info, **kwargs):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ user_data = [
|
|||
{
|
||||
'teacher': ('Nico', 'Zickgraf',),
|
||||
'class': 'FLID2018a',
|
||||
'code': 'XXXX',
|
||||
'students': [
|
||||
('Robin', 'Abbühl'),
|
||||
('Zeynep', 'Catal'),
|
||||
|
|
@ -15,12 +16,13 @@ user_data = [
|
|||
('Kelly', 'To'),
|
||||
('Deborah', 'Waldmeier'),
|
||||
('Rahel', 'Weiss'),
|
||||
('Nora', 'Zimmermann'),
|
||||
('Cindy', 'Zimmermann'),
|
||||
]
|
||||
},
|
||||
{
|
||||
'teacher': ('Michael', 'Gurtner',),
|
||||
'class': 'KF1A',
|
||||
'code': 'YYYY',
|
||||
'students': [
|
||||
('Lisa', 'Arn'),
|
||||
('Machado', 'Fernandes'),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from wagtail.core.models import Page
|
|||
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
|
||||
from core.factories import UserFactory
|
||||
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
|
||||
from users.services import create_users
|
||||
from users.models import Role
|
||||
from users.services import create_users, create_student
|
||||
|
||||
from .data.module_data import data
|
||||
from .data.user_data import user_data
|
||||
|
|
@ -57,6 +58,13 @@ class Command(BaseCommand):
|
|||
|
||||
create_users(user_data)
|
||||
|
||||
# create student without class
|
||||
create_student(
|
||||
username='hansli',
|
||||
first_name='Hansli',
|
||||
last_name='Alleini'
|
||||
)
|
||||
|
||||
for book_idx, book_data in enumerate(data):
|
||||
book = BookFactory.create(parent=site.root_page, **self.filter_data(book_data, 'topics'))
|
||||
|
||||
|
|
@ -102,8 +110,6 @@ class Command(BaseCommand):
|
|||
# ContentBlockFactory.create(parent=chapter, **self.filter_data(content_block_data, 'contents'))
|
||||
ContentBlockFactory.create(parent=chapter, module=module, **content_block_data)
|
||||
|
||||
|
||||
|
||||
# now create all and rooms
|
||||
management.call_command('dummy_rooms', verbosity=0)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ SIGNING_SECRET = os.environ.get('SIGNING_SECRET')
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = bool_value(os.environ.get('DEBUG', ''))
|
||||
TEST = 'test' in sys.argv
|
||||
ENABLE_SILKY = bool_value(os.environ.get('ENABLE_SILKY', ''))
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
|
|
@ -107,6 +108,11 @@ if DEBUG:
|
|||
)
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# enable silk for performance measuring
|
||||
if ENABLE_SILKY:
|
||||
INSTALLED_APPS += ['silk']
|
||||
MIDDLEWARE += ['silk.middleware.SilkyMiddleware', ]
|
||||
|
||||
MIDDLEWARE += [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% load i18n %}
|
||||
{% load core_tags %}
|
||||
{% autoescape off %}
|
||||
|
||||
{% blocktrans %}Du erhältst dieses E-Mail, weil dein Passwort auf {{ site_name }} zurückgesetzt wurde.{% endblocktrans %}
|
||||
|
||||
{% trans "Bitte öffnen Sie folgende Seite, um Ihr neues Passwort einzugeben:" %}
|
||||
{% block reset_link %}
|
||||
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
|
||||
<a href="{% reset_link 'password_reset_confirm' protocol domain token uid %}">{% reset_link 'password_reset_confirm' protocol domain token uid %}</a>
|
||||
{% endblock %}
|
||||
{% trans "Ihr Benutzername lautet:" %} {{ user.get_username }}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% load i18n %}
|
||||
{% load core_tags %}
|
||||
{% 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 %}
|
||||
<a href="{% reset_link 'set_password_confirm' protocol domain token uid %}">{% reset_link 'set_password_confirm' protocol domain token uid %}</a>
|
||||
{% endblock %}
|
||||
{% trans "Ihr Benutzername lautet:" %} {{ user.get_username }}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% load i18n %}
|
||||
{% load core_tags %}
|
||||
{% 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 %}
|
||||
<a href="{% reset_link 'set_password_confirm' protocol domain token uid %}">{% reset_link 'set_password_confirm' protocol domain token uid %}</a>
|
||||
{% endblock %}
|
||||
{% trans "Ihr Benutzername lautet:" %} {{ user.get_username }}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from rest_framework import serializers
|
||||
|
||||
register = template.Library()
|
||||
|
|
@ -18,4 +19,9 @@ def json_dumps(obj):
|
|||
|
||||
@register.filter(name='class_name')
|
||||
def class_name(obj):
|
||||
return str(obj.__class__.__name__)
|
||||
return str(obj.__class__.__name__)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def reset_link(name, protocol, domain, token, uid):
|
||||
return '{}://{}{}'.format(protocol, domain, reverse(name, kwargs={'uidb64': uid, 'token': token}))
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ if settings.DEBUG and not settings.USE_AWS:
|
|||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
if settings.ENABLE_SILKY:
|
||||
urlpatterns += [url(r'^silk/', include('silk.urls', namespace='silk'))]
|
||||
|
||||
# actually we use the cms in headless mode but need the url pattern to get the wagtail_serve function
|
||||
urlpatterns += [url(r'pages/', include(wagtail_urls)), ]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import graphene
|
||||
from django.db.models import Q
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
|
@ -29,17 +30,14 @@ class ObjectiveGroupNode(DjangoObjectType):
|
|||
user = info.context.user
|
||||
school_classes = user.school_classes.values_list('pk')
|
||||
|
||||
objectives_from_publisher = Q(owner=None)
|
||||
objectives_from_user = Q(owner=user)
|
||||
objectives_from_teacher = Q(owner__isnull=False, visible_for__in=school_classes)
|
||||
|
||||
if user.has_perm('users.can_manage_school_class_content'): # teacher
|
||||
publisher_objectives = self.objectives.filter(owner=None)
|
||||
user_created_objectives = self.objectives.filter(owner=user)
|
||||
return self.objectives.filter(objectives_from_publisher | objectives_from_user)
|
||||
else: # student
|
||||
publisher_objectives = self.objectives.filter(owner=None).exclude(
|
||||
hidden_for__in=school_classes)
|
||||
|
||||
user_created_objectives = self.objectives.filter(owner__isnull=False,
|
||||
visible_for__in=school_classes)
|
||||
|
||||
return publisher_objectives.union(user_created_objectives)
|
||||
return self.objectives.filter(objectives_from_publisher | objectives_from_teacher)
|
||||
|
||||
|
||||
class ObjectiveNode(DjangoObjectType):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class RoleInline(admin.TabularInline):
|
|||
|
||||
@admin.register(SchoolClass)
|
||||
class SchoolClassAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name')
|
||||
list_display = ('name', 'code', 'is_deleted')
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.1.15 on 2020-02-11 10:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0009_auto_20191009_0905'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='schoolclass',
|
||||
name='code',
|
||||
field=models.CharField(blank=True, default=None, max_length=10, null=True, unique=True, verbose_name='Code zum Beitreten'),
|
||||
),
|
||||
]
|
||||
|
|
@ -95,9 +95,14 @@ class SchoolClass(models.Model):
|
|||
name = models.CharField(max_length=100, blank=False, null=False, unique=True)
|
||||
is_deleted = models.BooleanField(blank=False, null=False, default=False)
|
||||
users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True)
|
||||
code = models.CharField('Code zum Beitreten', blank=True, null=True, max_length=10, unique=True, default=None)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Schulklasse'
|
||||
verbose_name_plural = 'Schulklassen'
|
||||
|
||||
def __str__(self):
|
||||
return 'SchoolClass {}-{}'.format(self.id, self.name)
|
||||
return '{}'.format(self.name)
|
||||
|
||||
@classmethod
|
||||
def generate_default_group_name(cls):
|
||||
|
|
@ -130,6 +135,11 @@ class SchoolClass(models.Model):
|
|||
def get_teacher(self):
|
||||
return self.users.filter(user_roles__role__key='teacher').first()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.code == '': # '' can't be unique, so we null it
|
||||
self.code = None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import graphene
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from graphene import relay
|
||||
|
||||
from api.utils import get_object
|
||||
from users.inputs import PasswordUpdateInput
|
||||
from users.models import SchoolClass, UserSetting
|
||||
from users.schema import SchoolClassNode
|
||||
from users.serializers import PasswordSerialzer, AvatarUrlSerializer
|
||||
|
||||
|
||||
class CodeNotFoundException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FieldError(graphene.ObjectType):
|
||||
code = graphene.String()
|
||||
|
||||
|
|
@ -102,8 +108,33 @@ class UpdateSetting(relay.ClientIDMutation):
|
|||
errors = graphene.List(UpdateError)
|
||||
|
||||
|
||||
class JoinClass(relay.ClientIDMutation):
|
||||
class Input:
|
||||
code = graphene.String(required=True)
|
||||
|
||||
success = graphene.Boolean()
|
||||
school_class = graphene.Field(SchoolClassNode)
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
user = info.context.user
|
||||
code = kwargs.get('code')
|
||||
try:
|
||||
school_class = SchoolClass.objects.get(Q(code__iexact=code))
|
||||
|
||||
|
||||
if user not in list(school_class.users.all()):
|
||||
school_class.users.add(user)
|
||||
else:
|
||||
raise CodeNotFoundException('[CAJ] Schüler ist bereits in Klasse') # CAJ = Class Already Joined
|
||||
|
||||
return cls(success=True, school_class=school_class)
|
||||
except SchoolClass.DoesNotExist:
|
||||
raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CAV = Code Not Valid
|
||||
|
||||
|
||||
class ProfileMutations:
|
||||
update_password = UpdatePassword.Field()
|
||||
update_avatar = UpdateAvatar.Field()
|
||||
update_setting = UpdateSetting.Field()
|
||||
|
||||
join_class = JoinClass.Field()
|
||||
|
|
|
|||
|
|
@ -3,11 +3,23 @@ from users.factories import SchoolClassFactory
|
|||
from users.models import Role, UserRole, DEFAULT_SCHOOL_ID
|
||||
|
||||
|
||||
def create_student(**kwargs):
|
||||
try:
|
||||
student_role = Role.objects.get_default_student_role()
|
||||
except Role.DoesNotExist:
|
||||
Role.objects.create_default_roles()
|
||||
student_role = Role.objects.get_default_student_role()
|
||||
user = UserFactory(
|
||||
**kwargs
|
||||
)
|
||||
UserRole.objects.create(user=user, role=student_role)
|
||||
return user
|
||||
|
||||
|
||||
def create_users(data=None):
|
||||
Role.objects.create_default_roles()
|
||||
|
||||
teacher_role = Role.objects.get_default_teacher_role()
|
||||
student_role = Role.objects.get_default_student_role()
|
||||
|
||||
if data is None:
|
||||
teacher = UserFactory(username='teacher')
|
||||
|
|
@ -15,18 +27,17 @@ def create_users(data=None):
|
|||
|
||||
students = []
|
||||
for i in range(1, 7):
|
||||
student = UserFactory(username='student{}'.format(i))
|
||||
UserRole.objects.create(user=student, role=student_role)
|
||||
student = create_student(username='student{}'.format(i))
|
||||
students.append(student)
|
||||
|
||||
SchoolClassFactory(
|
||||
users=[teacher] + students,
|
||||
name='skillbox'
|
||||
name='skillbox',
|
||||
)
|
||||
teacher2 = UserFactory(username='teacher2')
|
||||
UserRole.objects.create(user=teacher2, role=teacher_role)
|
||||
student_second_class = UserFactory(username='student_second_class')
|
||||
UserRole.objects.create(user=student_second_class, role=student_role)
|
||||
|
||||
student_second_class = create_student(username='student_second_class')
|
||||
SchoolClassFactory(
|
||||
users=[teacher2, student_second_class],
|
||||
name='second_class'
|
||||
|
|
@ -45,16 +56,16 @@ def create_users(data=None):
|
|||
students = []
|
||||
|
||||
for first, last in school_class.get('students'):
|
||||
student = UserFactory(
|
||||
student = create_student(
|
||||
username='{}.{}'.format(first, last).lower(),
|
||||
first_name=first,
|
||||
last_name=last,
|
||||
email='{}.{}@skillbox.example'.format(first, last).lower()
|
||||
)
|
||||
UserRole.objects.create(user=student, role=student_role)
|
||||
students.append(student)
|
||||
|
||||
SchoolClassFactory(
|
||||
users=students + [teacher],
|
||||
name=school_class.get('class'),
|
||||
code=school_class.get('code', '')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
from django.test import TestCase
|
||||
from graphene import Context
|
||||
from graphene.test import Client
|
||||
|
||||
from core.factories import UserFactory
|
||||
from users.factories import SchoolClassFactory
|
||||
from users.models import SchoolClass
|
||||
from api.schema import schema
|
||||
|
||||
|
||||
class JoinSchoolClassTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client(schema=schema)
|
||||
self.user = UserFactory(username='ueli')
|
||||
SchoolClassFactory(users=[self.user], name='Klasse 1A', code='XXXX')
|
||||
SchoolClassFactory(name='Klasse 2B', code='YYYY')
|
||||
self.mutation = """
|
||||
mutation JoinClass($input: JoinClassInput!) {
|
||||
joinClass(input: $input) {
|
||||
success
|
||||
schoolClass {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
self.variables = {
|
||||
'input': {
|
||||
'code': 'YYYY'
|
||||
}
|
||||
}
|
||||
self.context = Context(user=self.user)
|
||||
|
||||
def test_join_class(self):
|
||||
self.assertEqual(self.user.school_classes.count(), 1)
|
||||
|
||||
executed = self.client.execute(self.mutation, variables=self.variables, context=self.context)
|
||||
self.assertIsNone(executed.get('errors', None))
|
||||
result = executed['data']['joinClass']
|
||||
self.assertEqual(result['success'], True)
|
||||
self.assertEqual(result['schoolClass']['name'], 'Klasse 2B')
|
||||
self.assertEqual(self.user.school_classes.count(), 2)
|
||||
|
||||
def test_class_already_joined(self):
|
||||
code = 'YYYY'
|
||||
school_class = SchoolClass.objects.get(code=code)
|
||||
school_class.users.add(self.user)
|
||||
|
||||
self.assertEqual(self.user.school_classes.count(), 2)
|
||||
|
||||
executed = self.client.execute(self.mutation, variables=self.variables, context=self.context)
|
||||
self.assertIsNotNone(executed['errors'])
|
||||
self.assertEqual(executed['errors'][0]['message'], '[CAJ] Schüler ist bereits in Klasse')
|
||||
self.assertEqual(self.user.school_classes.count(), 2)
|
||||
|
||||
def test_code_not_valid(self):
|
||||
code = '1234'
|
||||
self.assertEqual(self.user.school_classes.count(), 1)
|
||||
|
||||
executed = self.client.execute(self.mutation, variables={
|
||||
'input': {
|
||||
'code': code
|
||||
}
|
||||
}, context=self.context)
|
||||
self.assertIsNotNone(executed['errors'])
|
||||
self.assertEqual(executed['errors'][0]['message'], '[CNV] Code ist nicht gültig')
|
||||
self.assertEqual(self.user.school_classes.count(), 1)
|
||||
Loading…
Reference in New Issue