Merged in feature/multiple-classes-for-everyone (pull request #46)

Feature/multiple classes for everyone

Approved-by: Christian Cueni
This commit is contained in:
Ramon Wenger 2020-02-25 08:42:21 +00:00
commit a7413c6ce0
40 changed files with 1061 additions and 168 deletions

View File

@ -10,6 +10,7 @@ python_version = '3.6'
awscli = "*" awscli = "*"
ipdb = "*" ipdb = "*"
coverage = "*" coverage = "*"
django-silk = "*"
[packages] [packages]
factory-boy = "==2.11.0" factory-boy = "==2.11.0"

177
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "e7807798e8b5c39d7c1bc57d61520f2c888da08d2b6061f07758e00b490fdbd6" "sha256": "ca4f635dc983134e4569df2af8f0d73f488211bc2a97d11c194f76279f942977"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -48,18 +48,18 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:27e221d3868f35687807e5c920f7e8d4872f722f64196a7fd274a06ad65beec0", "sha256:629ce3be236b6e0aed52358146eea9ffa7679d6cd1cc9b3e12332226270d6499",
"sha256:8ff4e3d9e5d6a26dd7494afc68dc96afe6b7bda88130cca84cd58702d888ed27" "sha256:b1351e62136fae29be8fcbb1c4890f1d72017d57e33051d435a8bf9f71212fde"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.11.10" "version": "==1.11.14"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:cf3144994191847e30ef76781af867009bdc233b3f1f4736615e5330687a891e", "sha256:34ad4d73e6bef5c8ad956c66354611628cdebd431fe2927261ed9c068b9cfb7a",
"sha256:f11ff8616f46ca04697df031e622c9ed50931b9d649d4e719f961e9d80771e8d" "sha256:6570f2ba046956d5b548ab2346d32f713ecf8b8cb098a839d73fcf832ccfa223"
], ],
"version": "==1.14.10" "version": "==1.14.14"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -535,10 +535,10 @@
}, },
"s3transfer": { "s3transfer": {
"hashes": [ "hashes": [
"sha256:2525bae2a530195576da53671bae8ca8c55ee8e33bc2225a65e804476611ea5a", "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
"sha256:4924e10451cc37901945806423d16c2c2040a6530645a614ed87e995ccec764c" "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
], ],
"version": "==0.3.2" "version": "==0.3.3"
}, },
"sendgrid": { "sendgrid": {
"hashes": [ "hashes": [
@ -603,6 +603,7 @@
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
], ],
"markers": "python_version != '3.4'",
"version": "==1.25.8" "version": "==1.25.8"
}, },
"wagtail": { "wagtail": {
@ -652,13 +653,26 @@
} }
}, },
"develop": { "develop": {
"asgiref": {
"hashes": [
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
"sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
],
"version": "==3.2.3"
},
"autopep8": {
"hashes": [
"sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"
],
"version": "==1.5"
},
"awscli": { "awscli": {
"hashes": [ "hashes": [
"sha256:4c49f085fb827ca1aeba5e6e5e39f6005110a0059b5c772aeb1d51c4f33c4028", "sha256:2748af4e77728ced50d7d5bda0fa980449bd71eedff90ee643bee86ed4283d2f",
"sha256:9459ac705c2a5d8724057492800c52084df714b624853eb3331087ecf8726a22" "sha256:9118015f4bbab1c671d9c9927d07b6f7eadb7e1e8bbb2b06dc849c3de578d692"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.9" "version": "==1.17.14"
}, },
"backcall": { "backcall": {
"hashes": [ "hashes": [
@ -669,17 +683,32 @@
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:cf3144994191847e30ef76781af867009bdc233b3f1f4736615e5330687a891e", "sha256:34ad4d73e6bef5c8ad956c66354611628cdebd431fe2927261ed9c068b9cfb7a",
"sha256:f11ff8616f46ca04697df031e622c9ed50931b9d649d4e719f961e9d80771e8d" "sha256:6570f2ba046956d5b548ab2346d32f713ecf8b8cb098a839d73fcf832ccfa223"
], ],
"version": "==1.14.10" "version": "==1.14.14"
},
"certifi": {
"hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
"version": "==2019.11.28"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
}, },
"colorama": { "colorama": {
"hashes": [ "hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
], ],
"version": "==0.4.1" "markers": "python_version != '3.4'",
"version": "==0.4.3"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
@ -725,6 +754,22 @@
], ],
"version": "==4.4.1" "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": { "docutils": {
"hashes": [ "hashes": [
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
@ -733,6 +778,19 @@
], ],
"version": "==0.15.2" "version": "==0.15.2"
}, },
"gprof2dot": {
"hashes": [
"sha256:b43fe04ebb3dfe181a612bbfc69e90555b8957022ad6a466f0308ed9c7f22e99"
],
"version": "==2019.11.30"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"ipdb": { "ipdb": {
"hashes": [ "hashes": [
"sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd" "sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"
@ -762,6 +820,13 @@
], ],
"version": "==0.16.0" "version": "==0.16.0"
}, },
"jinja2": {
"hashes": [
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250",
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"
],
"version": "==2.11.1"
},
"jmespath": { "jmespath": {
"hashes": [ "hashes": [
"sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6",
@ -769,6 +834,44 @@
], ],
"version": "==0.9.4" "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": { "parso": {
"hashes": [ "hashes": [
"sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57", "sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57",
@ -812,6 +915,13 @@
], ],
"version": "==0.4.8" "version": "==0.4.8"
}, },
"pycodestyle": {
"hashes": [
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
],
"version": "==2.5.0"
},
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
@ -826,6 +936,13 @@
], ],
"version": "==2.8.1" "version": "==2.8.1"
}, },
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
@ -842,6 +959,14 @@
], ],
"version": "==5.2" "version": "==5.2"
}, },
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"index": "pypi",
"version": "==2.22.0"
},
"rsa": { "rsa": {
"hashes": [ "hashes": [
"sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5", "sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5",
@ -851,10 +976,10 @@
}, },
"s3transfer": { "s3transfer": {
"hashes": [ "hashes": [
"sha256:2525bae2a530195576da53671bae8ca8c55ee8e33bc2225a65e804476611ea5a", "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
"sha256:4924e10451cc37901945806423d16c2c2040a6530645a614ed87e995ccec764c" "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db"
], ],
"version": "==0.3.2" "version": "==0.3.3"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -863,6 +988,13 @@
], ],
"version": "==1.14.0" "version": "==1.14.0"
}, },
"sqlparse": {
"hashes": [
"sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
"sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
],
"version": "==0.3.0"
},
"traitlets": { "traitlets": {
"hashes": [ "hashes": [
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
@ -875,6 +1007,7 @@
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
], ],
"markers": "python_version != '3.4'",
"version": "==1.25.8" "version": "==1.25.8"
}, },
"wcwidth": { "wcwidth": {

View File

@ -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": []
}
}

View File

@ -1500,6 +1500,79 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "myInstrumentActivity",
"description": null,
"args": [
{
"name": "before",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "slug",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "type",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "InstrumentNodeConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "node", "name": "node",
"description": "The ID of the object", "description": "The ID of the object",
@ -3277,6 +3350,18 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "code",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "hiddenContentBlocks", "name": "hiddenContentBlocks",
"description": null, "description": null,
@ -9285,6 +9370,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "addProject",
"description": null, "description": null,
@ -11434,6 +11546,88 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "OBJECT",
"name": "AddProjectPayload", "name": "AddProjectPayload",

View File

@ -1,4 +1,4 @@
describe('Survey', () => { describe('Bookmarks', () => {
beforeEach(() => { beforeEach(() => {
// todo: mock all the graphql queries and mutations // todo: mock all the graphql queries and mutations
cy.exec("python ../server/manage.py prepare_bookmarks_for_cypress"); cy.exec("python ../server/manage.py prepare_bookmarks_for_cypress");
@ -27,7 +27,6 @@ describe('Survey', () => {
cy.get('@interviewContent').within(() => { cy.get('@interviewContent').within(() => {
cy.get('.bookmark-actions__edit-note').click(); cy.get('.bookmark-actions__edit-note').click();
}); });
cy.get('[data-cy=bookmark-note]').within(() => { cy.get('[data-cy=bookmark-note]').within(() => {

View File

@ -0,0 +1,48 @@
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=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);
})
})

View File

@ -10,7 +10,6 @@ describe('Survey', () => {
}); });
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.apolloLogin('rahel.cueni', 'test'); cy.apolloLogin('rahel.cueni', 'test');
}); });

View File

@ -1,19 +1,21 @@
<template> <template>
<div class="class-selection" v-if="isTeacher"> <div class="class-selection" v-if="currentClassSelection">
<div class="class-selection__selected-class selected-class" @click="showPopover = !showPopover"> <div data-cy="class-selection" class="class-selection__selected-class selected-class"
@click="showPopover = !showPopover">
<p class="selected-class__text">Klasse: {{currentClassSelection.name}}</p> <p class="selected-class__text">Klasse: {{currentClassSelection.name}}</p>
</div> </div>
<widget-popover v-if="showPopover" <widget-popover v-if="showPopover"
@hide-me="showPopover = false" @hide-me="showPopover = false"
:mobile="mobile" :mobile="mobile"
class="class-selection__popover"> class="class-selection__popover">
<li class="popover-links__link popover-links__link--large" v-for="schoolClass in schoolClasses" <li data-cy="class-selection-entry" class="popover-links__link popover-links__link--large"
v-for="schoolClass in schoolClasses"
:key="schoolClass.id" :key="schoolClass.id"
:label="schoolClass.name" :label="schoolClass.name"
:item="schoolClass" :item="schoolClass"
@click="updateFilter(schoolClass)"> @click="updateFilter(schoolClass)">
{{schoolClass.name}} {{schoolClass.name}}
</li> </li>
</widget-popover> </widget-popover>
</div> </div>
</template> </template>
@ -47,7 +49,8 @@
selectedClass: { selectedClass: {
id: '' id: ''
}, },
permissions: [] permissions: [],
schoolClasses: []
}, },
showPopover: false showPopover: false
} }
@ -78,14 +81,11 @@
currentClassSelection() { currentClassSelection() {
let currentClass = this.schoolClasses.find(schoolClass => { let currentClass = this.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id return schoolClass.id === this.me.selectedClass.id
}) });
return currentClass || this.schoolClasses[0]; return currentClass || this.schoolClasses[0];
}, },
schoolClasses() { schoolClasses() {
return this.$getRidOfEdges(this.me.schoolClasses); return this.$getRidOfEdges(this.me.schoolClasses);
},
isTeacher() {
return this.me.permissions.includes('users.can_manage_school_class_content');
} }
}, },
@ -113,8 +113,8 @@
line-height: $large-spacing; line-height: $large-spacing;
@include regular-text; @include regular-text;
color: $color-silver-dark; color: $color-silver-dark;
}
} }
}
.popover-links__link { .popover-links__link {
cursor: pointer; cursor: pointer;

View File

@ -59,15 +59,8 @@
display: flex; display: flex;
&__link { &__link {
font-size: 1.0625rem;
padding: 0 24px; padding: 0 24px;
font-family: $sans-serif-font-family; @include default-link;
font-weight: $font-weight-regular;
color: $color-silver-dark;
&--active {
color: $color-brand;
}
} }
$parent: &; $parent: &;

View File

@ -1,6 +1,6 @@
<template> <template>
<header class="header-bar"> <header class="header-bar">
<top-navigation></top-navigation> <content-navigation></content-navigation>
<router-link to="/" class="header-bar__logo" data-cy="home-link"> <router-link to="/" class="header-bar__logo" data-cy="home-link">
<logo></logo> <logo></logo>
</router-link> </router-link>
@ -14,7 +14,7 @@
</template> </template>
<script> <script>
import TopNavigation from '@/components/TopNavigation.vue'; import ContentNavigation from '@/components/ContentNavigation.vue';
import BookNavigation from '@/components/book-navigation/BookNavigation'; import BookNavigation from '@/components/book-navigation/BookNavigation';
import UserWidget from '@/components/UserWidget.vue'; import UserWidget from '@/components/UserWidget.vue';
import LogoutWidget from '@/components/LogoutWidget.vue'; import LogoutWidget from '@/components/LogoutWidget.vue';
@ -25,7 +25,7 @@
export default { export default {
components: { components: {
TopNavigation, ContentNavigation,
UserWidget, UserWidget,
LogoutWidget, LogoutWidget,
BookNavigation, BookNavigation,

View File

@ -1,36 +1,17 @@
<template> <template>
<div class="user-widget" :class="{'user-widget--is-profile': isProfile}"> <div class="user-widget" :class="{'user-widget--is-profile': isProfile}">
<div class="user-widget__avatar" @click="toggleShowPopover()"> <div class="user-widget__avatar" @click="openSidebar()">
<avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile"/> <avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile"/>
</div> </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> </div>
</template> </template>
<script> <script>
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql'; import TOGGLE_SIDEBAR from '@/graphql/gql/local/mutations/toggleSidebar.gql';
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
import WidgetPopover from '@/components/WidgetPopover';
export default { export default {
// todo: clean up unneeded props
props: { props: {
firstName: { firstName: {
type: String type: String
@ -51,31 +32,19 @@
} }
}, },
data() {
return {
showPopover: false
}
},
methods: { methods: {
toggleShowPopover() { openSidebar() {
if (this.showMenu) {
this.showPopover = !this.showPopover;
}
},
logout() {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: LOGOUT_MUTATION, mutation: TOGGLE_SIDEBAR,
}).then(({data}) => { variables: {
if (data.logout.success) { open: true
location.replace('/logout')
} }
}); });
} },
}, },
components: { components: {
Avatar, WidgetPopover Avatar
}, },
computed: { computed: {
isProfile() { isProfile() {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mobile-navigation"> <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"> <div class="mobile-navigation__close-button" @click="hideMobileNavigation">
<cross class="mobile-navigation__close-icon"></cross> <cross class="mobile-navigation__close-icon"></cross>
</div> </div>
@ -16,14 +16,14 @@
import Cross from '@/components/icons/Cross'; import Cross from '@/components/icons/Cross';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import LogoutWidget from '@/components/LogoutWidget'; import LogoutWidget from '@/components/LogoutWidget';
import TopNavigation from '@/components/TopNavigation'; import ContentNavigation from '@/components/ContentNavigation';
import ClassSelectionWidget from '@/components/ClassSelectionWidget'; import ClassSelectionWidget from '@/components/ClassSelectionWidget';
import {meQuery} from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
export default { export default {
components: { components: {
TopNavigation, ContentNavigation,
Cross, Cross,
UserWidget, UserWidget,
LogoutWidget, LogoutWidget,

View File

@ -89,7 +89,6 @@
spellcheckText() { spellcheckText() {
if (!this.spellcheckLoading) { if (!this.spellcheckLoading) {
return 'Rechtschreibung prüfen' return 'Rechtschreibung prüfen'
} else { } else {
return 'Wird geprüft...' return 'Wird geprüft...'
} }

View File

@ -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>

View File

@ -1,28 +1,49 @@
<template> <template>
<div class="avatar"> <div class="avatar">
<transition name="fade"> <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>
<transition name="show"> <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> </transition>
<img class="avatar__fake-image" :src="avatarUrl" ref="fakeImage"/> <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> </div>
</template> </template>
<script> <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 { export default {
props: ['avatarUrl', 'iconHighlighted'], props: {
components: {UserIcon}, avatarUrl: {
data () { type: String
},
iconHighlighted: {},
editable: {
default: false
}
},
components: {
DefaultAvatar,
PenIcon
},
data() {
return { return {
isAvatarLoaded: false isAvatarLoaded: false
} }
}, },
mounted () { mounted() {
if (this.avatarUrl !== '') { if (this.avatarUrl !== '') {
this.$refs.fakeImage.addEventListener('load', () => { this.$refs.fakeImage.addEventListener('load', () => {
if (this.$refs.fakeImage) { if (this.$refs.fakeImage) {
@ -30,7 +51,17 @@
this.isAvatarLoaded = true; this.isAvatarLoaded = true;
} }
}); });
}; }
},
methods: {
closeSidebar() {
this.$apollo.mutate({
mutation: TOGGLE_SIDEBAR,
variables: {
open: false
}
})
}
} }
} }
</script> </script>
@ -45,11 +76,11 @@
width: $max-width; width: $max-width;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
border-radius: 50%;
&__placeholder { &__placeholder {
height: $max-width; height: $max-width;
fill: $color-silver-dark; fill: $color-silver-dark;
border-radius: 50%;
&--highlighted { &--highlighted {
fill: $color-brand; fill: $color-brand;
@ -57,14 +88,14 @@
} }
&__image { &__image {
background-size: cover;
background-position: center center;
height: 100%;
width: 100%;
border: 0;
border-radius: 50%;
background-size: cover; &--landscape {
background-position: center center;
height: 100%;
width: 100%;
border: 0;
&--landscape {
width: auto; width: auto;
height: $max-width; height: $max-width;
} }
@ -75,10 +106,25 @@
height: 0; 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; transition: opacity .5s;
} }
.fade-leave-to, .show-enter { .fade-leave-to, .show-enter {
opacity: 0; opacity: 0;
} }

View File

@ -0,0 +1,111 @@
<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" @click="closeSidebar">
<router-link :to="{name: 'my-classes'}" class="profile-sidebar__link">Klassenliste</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';
export default {
components: {
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;
}
&__link {
@include default-link;
displaY: block;
padding: $large-spacing $medium-spacing;
}
&__close-link {
position: absolute;
right: $small-spacing;
top: $small-spacing;
cursor: pointer;
}
&__close-icon {
width: 40px;
height: 40px;
}
}
</style>

View File

@ -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>

View File

@ -102,6 +102,10 @@ export default function (uri) {
scrollPosition: { scrollPosition: {
__typename: 'ScrollPosition', __typename: 'ScrollPosition',
scrollTo: '' scrollTo: ''
},
sidebar: {
__typename: 'Sidebar',
open: false
} }
} }
}); });

View File

@ -0,0 +1,3 @@
mutation($open: Boolean!) {
toggleSidebar(open: $open) @client
}

View File

@ -0,0 +1,5 @@
query Sidebar {
sidebar @client {
open
}
}

View File

@ -0,0 +1,9 @@
mutation JoinClass($input: JoinClassInput!) {
joinClass(input: $input) {
success
schoolClass {
id
name
}
}
}

View File

@ -1,4 +1,5 @@
import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql'; import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql';
import SIDEBAR from '@/graphql/gql/local/sidebar.gql';
export const resolvers = { export const resolvers = {
Mutation: { Mutation: {
@ -7,6 +8,12 @@ export const resolvers = {
data.scrollPosition.scrollTo = scrollTo; data.scrollPosition.scrollTo = scrollTo;
cache.writeQuery({query: SCROLL_POSITION, data}); cache.writeQuery({query: SCROLL_POSITION, data});
return data.scrollPosition; return data.scrollPosition;
},
toggleSidebar: (_, {open}, {cache}) => {
const data = cache.readQuery({query: SIDEBAR});
data.sidebar.open = open;
cache.writeQuery({query: SIDEBAR, data});
return data.sidebar;
} }
} }
}; };

View File

@ -5,6 +5,10 @@ export const typeDefs = gql`
scrollTo: String! scrollTo: String!
} }
type Sidebar {
open: Boolean!
}
type Mutation { type Mutation {
scrollTo(scrollTo: String!): ScrollPosition scrollTo(scrollTo: String!): ScrollPosition
} }

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="blank-layout"> <div class="blank-layout">
<profile-sidebar></profile-sidebar>
<router-view></router-view> <router-view></router-view>
</div> </div>
</template> </template>
@ -14,9 +15,9 @@
</style> </style>
<script> <script>
import ProfileSidebar from '@/components/profile/ProfileSidebar';
export default { export default {
components: {}, components: {ProfileSidebar},
} }
</script> </script>

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="container skillbox" :class="specialContainerClass"> <div class="container skillbox" :class="specialContainerClass">
<profile-sidebar></profile-sidebar>
<header-bar class="header skillbox__header"> <header-bar class="header skillbox__header">
</header-bar> </header-bar>
@ -13,11 +14,13 @@
<script> <script>
import HeaderBar from '@/components/HeaderBar'; import HeaderBar from '@/components/HeaderBar';
import MobileHeader from '@/components/MobileHeader'; import MobileHeader from '@/components/MobileHeader';
import ProfileSidebar from '@/components/profile/ProfileSidebar';
export default { export default {
components: { components: {
HeaderBar, HeaderBar,
MobileHeader MobileHeader,
ProfileSidebar
}, },
computed: { computed: {

View File

@ -0,0 +1,84 @@
<template>
<div>
<h1>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 = 'Klasse bereits beigetreten';
} else {
this.error = 'Code ist nicht gültig';
}
})
},
cancel() {
this.$router.go(-1);
}
}
}
</script>

View File

@ -2,13 +2,16 @@
<div class="profile"> <div class="profile">
<nav class="top-navigation profile-submenu profile__submenu"> <nav class="top-navigation profile-submenu profile__submenu">
<router-link to="/me/activity" active-class="top-navigation__link--active" <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>
<router-link to="/me/myclasses" active-class="top-navigation__link--active" <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>
<router-link to="/me/profile" active-class="top-navigation__link--active" <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> </router-link>
</nav> </nav>
<router-view></router-view> <router-view></router-view>

View File

@ -30,6 +30,7 @@ import moduleRoom from '@/pages/moduleRoom'
import login from '@/pages/login' import login from '@/pages/login'
import registration from '@/pages/registration' import registration from '@/pages/registration'
import waitForClass from '@/pages/waitForClass' import waitForClass from '@/pages/waitForClass'
import joinClass from '@/pages/joinClass'
import store from '@/store/index'; import store from '@/store/index';
@ -112,6 +113,7 @@ const routes = [
{path: '', name: 'profile-activity', component: activity, meta: {isProfile: true}}, {path: '', name: 'profile-activity', component: activity, meta: {isProfile: true}},
] ]
}, },
{path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'simple'}},
{ {
path: '/survey/:id', path: '/survey/:id',
component: surveyPage, component: surveyPage,

View File

@ -141,6 +141,17 @@
font-size: toRem(18px); 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 { @mixin page-form-input-heading {
display: block; display: block;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;

View File

@ -1,4 +1,5 @@
import graphene import graphene
from django.db.models import Q
from graphene import relay from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
@ -85,24 +86,20 @@ class ChapterNode(DjangoObjectType):
def resolve_content_blocks(self, info, **kwargs): def resolve_content_blocks(self, info, **kwargs):
user = info.context.user user = info.context.user
school_classes = user.school_classes.values_list('pk') school_classes = user.school_classes.values_list('pk')
by_parent = ContentBlock.get_by_parent(self).prefetch_related(
'visible_for').prefetch_related( by_parent = ContentBlock.get_by_parent(self) \
'hidden_for') .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 if user.has_perm('users.can_manage_school_class_content'): # teacher
publisher_content_blocks = by_parent.filter(user_created=False) return by_parent.filter(default_blocks | owned_by_user)
user_created_content_blocks = by_parent.filter(user_created=True, owner=user)
else: # student else: # student
publisher_content_blocks = by_parent.filter(user_created=False).exclude( return by_parent.filter(default_blocks | teacher_created_and_visible)
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)
def resolve_bookmark(self, info, **kwags): def resolve_bookmark(self, info, **kwags):
return ChapterBookmark.objects.filter( return ChapterBookmark.objects.filter(
@ -234,24 +231,6 @@ class BookNode(DjangoObjectType):
return Topic.get_by_parent(self) 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): class BookQuery(object):
book = relay.Node.Field(BookNode) book = relay.Node.Field(BookNode)
topic = graphene.Field(TopicNode, slug=graphene.String()) topic = graphene.Field(TopicNode, slug=graphene.String())
@ -287,7 +266,6 @@ class BookQuery(object):
elif slug is not None: elif slug is not None:
module = Module.objects.get(slug=slug) module = Module.objects.get(slug=slug)
return module return module
def resolve_topic(self, info, **kwargs): def resolve_topic(self, info, **kwargs):

View File

@ -2,6 +2,7 @@ user_data = [
{ {
'teacher': ('Nico', 'Zickgraf',), 'teacher': ('Nico', 'Zickgraf',),
'class': 'FLID2018a', 'class': 'FLID2018a',
'code': 'XXXX',
'students': [ 'students': [
('Robin', 'Abbühl'), ('Robin', 'Abbühl'),
('Zeynep', 'Catal'), ('Zeynep', 'Catal'),
@ -15,12 +16,13 @@ user_data = [
('Kelly', 'To'), ('Kelly', 'To'),
('Deborah', 'Waldmeier'), ('Deborah', 'Waldmeier'),
('Rahel', 'Weiss'), ('Rahel', 'Weiss'),
('Nora', 'Zimmermann'), ('Cindy', 'Zimmermann'),
] ]
}, },
{ {
'teacher': ('Michael', 'Gurtner',), 'teacher': ('Michael', 'Gurtner',),
'class': 'KF1A', 'class': 'KF1A',
'code': 'YYYY',
'students': [ 'students': [
('Lisa', 'Arn'), ('Lisa', 'Arn'),
('Machado', 'Fernandes'), ('Machado', 'Fernandes'),

View File

@ -33,6 +33,7 @@ SIGNING_SECRET = os.environ.get('SIGNING_SECRET')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool_value(os.environ.get('DEBUG', '')) DEBUG = bool_value(os.environ.get('DEBUG', ''))
TEST = 'test' in sys.argv TEST = 'test' in sys.argv
ENABLE_SILKY = bool_value(os.environ.get('ENABLE_SILKY', ''))
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
@ -107,6 +108,11 @@ if DEBUG:
) )
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
# enable silk for performance measuring
if ENABLE_SILKY:
INSTALLED_APPS += ['silk']
MIDDLEWARE += ['silk.middleware.SilkyMiddleware', ]
MIDDLEWARE += [ MIDDLEWARE += [
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',

View File

@ -48,6 +48,9 @@ if settings.DEBUG and not settings.USE_AWS:
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 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 # 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)), ] urlpatterns += [url(r'pages/', include(wagtail_urls)), ]

View File

@ -1,4 +1,5 @@
import graphene import graphene
from django.db.models import Q
from graphene import relay from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
@ -29,17 +30,14 @@ class ObjectiveGroupNode(DjangoObjectType):
user = info.context.user user = info.context.user
school_classes = user.school_classes.values_list('pk') 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 if user.has_perm('users.can_manage_school_class_content'): # teacher
publisher_objectives = self.objectives.filter(owner=None) return self.objectives.filter(objectives_from_publisher | objectives_from_user)
user_created_objectives = self.objectives.filter(owner=user)
else: # student else: # student
publisher_objectives = self.objectives.filter(owner=None).exclude( return self.objectives.filter(objectives_from_publisher | objectives_from_teacher)
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)
class ObjectiveNode(DjangoObjectType): class ObjectiveNode(DjangoObjectType):

View File

@ -15,7 +15,7 @@ class RoleInline(admin.TabularInline):
@admin.register(SchoolClass) @admin.register(SchoolClass)
class SchoolClassAdmin(admin.ModelAdmin): class SchoolClassAdmin(admin.ModelAdmin):
list_display = ('id', 'name') list_display = ('name', 'code', 'is_deleted')
@admin.register(Role) @admin.register(Role)

View File

@ -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'),
),
]

View File

@ -70,9 +70,14 @@ class SchoolClass(models.Model):
name = models.CharField(max_length=100, blank=False, null=False, unique=True) name = models.CharField(max_length=100, blank=False, null=False, unique=True)
is_deleted = models.BooleanField(blank=False, null=False, default=False) is_deleted = models.BooleanField(blank=False, null=False, default=False)
users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True) 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): def __str__(self):
return 'SchoolClass {}-{}'.format(self.id, self.name) return '{}'.format(self.name)
@classmethod @classmethod
def generate_default_group_name(cls): def generate_default_group_name(cls):
@ -99,6 +104,11 @@ class SchoolClass(models.Model):
def get_teacher(self): def get_teacher(self):
return self.users.filter(user_roles__role__key='teacher').first() 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): class Role(models.Model):
key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True) key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True)
@ -165,4 +175,3 @@ class UserRole(models.Model):
class UserSetting(models.Model): class UserSetting(models.Model):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting') user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting')
selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE) selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE)

View File

@ -1,14 +1,20 @@
import graphene import graphene
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import Q
from graphene import relay from graphene import relay
from api.utils import get_object from api.utils import get_object
from users.inputs import PasswordUpdateInput from users.inputs import PasswordUpdateInput
from users.models import SchoolClass, UserSetting from users.models import SchoolClass, UserSetting
from users.schema import SchoolClassNode
from users.serializers import PasswordSerialzer, AvatarUrlSerializer from users.serializers import PasswordSerialzer, AvatarUrlSerializer
class CodeNotFoundException(Exception):
pass
class FieldError(graphene.ObjectType): class FieldError(graphene.ObjectType):
code = graphene.String() code = graphene.String()
@ -102,8 +108,33 @@ class UpdateSetting(relay.ClientIDMutation):
errors = graphene.List(UpdateError) 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: class ProfileMutations:
update_password = UpdatePassword.Field() update_password = UpdatePassword.Field()
update_avatar = UpdateAvatar.Field() update_avatar = UpdateAvatar.Field()
update_setting = UpdateSetting.Field() update_setting = UpdateSetting.Field()
join_class = JoinClass.Field()

View File

@ -21,7 +21,7 @@ def create_users(data=None):
SchoolClassFactory( SchoolClassFactory(
users=[teacher] + students, users=[teacher] + students,
name='skillbox' name='skillbox',
) )
teacher2 = UserFactory(username='teacher2') teacher2 = UserFactory(username='teacher2')
UserRole.objects.create(user=teacher2, role=teacher_role) UserRole.objects.create(user=teacher2, role=teacher_role)
@ -57,4 +57,5 @@ def create_users(data=None):
SchoolClassFactory( SchoolClassFactory(
users=students + [teacher], users=students + [teacher],
name=school_class.get('class'), name=school_class.get('class'),
code=school_class.get('code', '')
) )

View File

@ -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)