diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11627c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +e2e/screenshots/login* +!e2e/screenshots/login/.gitkeep + +e2e/screenshots/admin/lessons/* +!e2e/screenshots/admin/lessons/.gitkeep + +e2e/screenshots/admin/questions/* +!e2e/screenshots/admin/questions/.gitkeep \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c619b2 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Programmero + +De documentatie kan worden gevonden onder documentation/Software Guidebook. + +#### Installatie handleiding + +##### Voorwoord + +Op het moment van schrijven is de software nog niet klaar om op grote schaal in productie te worden genomen, desondanks wordt er in dit hoofdstuk duidelijk wat er moet gebeuren om dit te realiseren. + +##### Security overwegingen vóór productie. + +In programmero wordt er altijd met wachtwoorden gewerkt. Deze handleiding geeft géén instructies om een SSL (voor https) certificaat te installeren, om de server in productie te laten draaien is dit een vereiste. + +Ook is er nog geen rekening gehouden met brute force attacks, DDoS aanvallen etc. als de server niet op een geïsoleerd netwerk draait is het van belang dat deze problemen worden verholpen. + +Momenteel kan een gebruiker zijn gegevens (email, wachtwoord) nog niet veranderen, dit is essentieel voor de veiligheid van een applicatie. + +**Pre condities server** + +Als database gebruikt programmero [mongo](https://www.mongodb.com/download-center/community), de database engine moet draaien. + +De volgende programma's dienen op de server te staan (kunnen op de terminal worden uitgevoerd): + +- [npm](https://nodejs.org/en/) +- [node](https://nodejs.org/en/) +- [nodemon (voor debuggen)](https://nodemon.io/) +- [git](https://git-scm.com/) + +**Installatiehandleiding** + +Wanneer er iets fout is zal npm rode "ERROR" berichten geven. Wanneer deze niet in de output zitten is het aannemelijk dat alles goed is gegaan. + +1. Clone de repository: + +``` +git clone https://github.com/HANICA-DWA/sep2018-project-aardvark +``` + +**Zorg dat je in [programmero-backend](https://github.com/HANICA-DWA/sep2018-project-aardvark/tree/development/programmero-backend) zit:** + +``` +npm i +``` + +2. Maak een .env bestand aan door .env.example te kopiëren, zorg dat alle variabelen in het .env bestand goed ingevuld zijn. + +Windows: + +``` +copy .env.example .env +``` + +Mac / linux + +``` +cp .env.example .env +``` + +4. Voer het database bestand uit zodat de database gevuld wordt met een admin en student account, wanneer je "Database seeded!" ziet is het goed gegaan. + +``` +node databaseSeeder.js +``` + +**Zorg dat je in [programmero-frontend](https://github.com/HANICA-DWA/sep2018-project-aardvark/tree/development/programmero-frontend) zit:** + +``` +npm i +``` + +**Operationele handleiding** + +Zorg dat mongodb draait, start vervolgens de backend op: + +**Zorg dat je in [programmero-backend](https://github.com/HANICA-DWA/sep2018-project-aardvark/tree/development/programmero-backend) zit:** + +``` +npm start +``` + +Start een nieuw terminal venster op + +**Zorg dat je in [programmero-frontend](https://github.com/HANICA-DWA/sep2018-project-aardvark/tree/development/programmero-frontend) zit:** + +``` +npm start +``` + +Wanneer men nu [localhost:3000](http://localhost:3000) intikt in de browser zal men als het goed is de loginpagina moeten zien van programmero. Er kan worden ingelogd met admin@admin.nl en het wachtwoord test + +**Automatisch testen** + +Er zijn automatische testen beschikbaar, zie [hoofdstuk 8](https://github.com/HANICA-DWA/sep2018-project-aardvark/blob/development/documentation/Software%20Guidebook/chapters/8_Principes.md) van het softwareguidebook om de handleiding hiervan te bekijken. \ No newline at end of file diff --git a/documentation/Hoe werkt git.md b/documentation/Hoe werkt git.md new file mode 100644 index 0000000..2e4c664 --- /dev/null +++ b/documentation/Hoe werkt git.md @@ -0,0 +1,61 @@ +## Hoe werkt git? + +Kijk tijdens het uitvoeren van git pull en git merge altijd of er geen CONFLICT in de console verschijnt! + +![alt merge conflicten](https://www.mupload.nl/img/bhg5c1k5t9a.jpg) + +### Nieuwe branch aanmaken + +``` +git branch +``` + +Dit moet development zijn, zo niet: + +``` +git checkout development +``` + +Vervolgens doe je: + +``` +git fetch --all +git pull +``` + +Kijk of er geen conflicten in de branch staan en maak vervolgens de branch aan. + +``` +git checkout -b feature/ +``` + +### Aan de branch werken + +Zorg dat je in de root van de repository bent. + +``` +git status +``` + +Controleer of jouw werkt (ongeveer) overeenkomt met de bestanden, voer vervolgens de volgende riddle uit. + +``` +git add . +git commit -m "Zeg welke delen code je hebt toegevoegd" +git push +``` + + + +### Het up-to-date houden van de code (dagelijks doen!) + +Zorg dat je alle wijzigingen hebt gecommit. Hier zouden geen problemen bij mogen ontstaan. Mocht dit wel gebeuren, vraag om hulp. Deze actie moet je altijd uitvoeren voordat je een pull request aanmaakt op github! + +``` +git status +git checkout development +git fetch --all +git pull +git checkout +git merge development +``` \ No newline at end of file diff --git a/documentation/Software Guidebook/C4 Diagrammen/Component diagram back-end.xml b/documentation/Software Guidebook/C4 Diagrammen/Component diagram back-end.xml new file mode 100644 index 0000000..a1bcd4f --- /dev/null +++ b/documentation/Software Guidebook/C4 Diagrammen/Component diagram back-end.xml @@ -0,0 +1 @@ +5VxZc+I4EP41PEJZli8eAzmmtiYzqWR2Z/O0pWABTozFyOKaX7+SLWOMjOMsGCtLXrBarcPq/r7W5XTgcLa+o2g+vSc+Djum4a878LpjmsDy+vxHSDapxIVuKpjQwJdKueAp+I2l0JDSReDjuKDICAlZMC8KRySK8IgVZIhSsiqqjUlYbHWOJlgRPI1QqEp/Bj6bplJoGEae8QUHk6ls2tzmzFCmLQXxFPlktSOCNx04pISw9Gm2HuJQjF42MGm52wO5255RHLE6Bb5d37nfx1Y0/+Pnd+fmn+5yGP3oAiDrWaJwId9ZdpdtskGgZBH5WFRjdOBgNQ0YfpqjkchdcbNz2ZTNQp4C/NFH8TTRFQlZN6YMrw92G2wHg7sRJjPM6IaryAKeLcdPepDp2ml6ldsDwL5Umu7YApiWVEXSCybb2vNx4g9yqD4ybEa/ZNickLc8GBP+Yrvj5/xakCyjGycufsUVTHO+zjP500T8PlAyoWg2w5RkFb7QLHOARm9dHPmi+k3MMJ5lOvwt0nZTRcWEfPxZ0U4xo+QND0lIKJdEJMKii0EY7olQGEwinhxxe2EuHwhrBhwhVzJjFvi+aKbUMYquc2rf6BtF3wDQMxTn4Jyj+kbmL6f3DBMqnvHlx48HLnnEvxY4ZoptsM8pRyYJZVMyIREKb3Lp3ijuGBGvA/a3EPdsmXreybleyxJJYiMTr5ixjWRatGCEi/JWvxIyL9hHdK7aOvxdyIKOcCXLyOFmiE4wq9S0Ki3eNXrWlrMoDhELlsX+lVlUVvdAggSamQoZj2PMFJNvWz3CC7IXPoIfgFfGD7dUaJVyQM4THXsw5GooiDhi7WtV4RGjkejEEjg9pwfLFPzFWijYPdADJS0clLzHRO+EDxTP0zg+DtbC409BEwCaVpEngBpCnLII4jRGE0CliWwE/WBZ6h+Cw7uSjoWDSEZWXCSrJ56jqJajOWWOdo0YekEx3rFrWqFi7qS/RWnRA5p4rUpH/1BfT9WvUig00N49iSbkeiB8BwpwGq10gAjPEPTA+2BXt6clIxhekREcqM4cQN8s4YRMeHpOgJbCCXf4hS6CN1Zm49NPI7Ln58KUQqNphFV7GmGX27/2lOFIdlctycdoSmjwmzdPot5rXBq175O59IqPTWnY1gM6e8EUuiXIOXM0Ne0K5FwgTuzaOPHaxYlqN/4Y+AlIhlM8esM0/sxgsb3iClULsHhNYEL7JahXFxOHbHomTJjq1lKTXPb/sRvoV2Kxa/RcA0rC0XnrwCudO/CRC0affvLgQUc7PrSAMjSXgKt+XVylJmuNDy11p+QgH6rb3BTHfM79kiiIoZ4LICd9tAcCJh/Z4A7RCw7FHvwksX62Ud4x4Tj5q8KIPKCSPelsT4V2bVflnlW0BhzTPY7WJBq7YK9IgzSX4bm9mTkfd7rZFhKJnVIimRdLUp8DrbbZKlqBOnsJcRyT6OA0/pEsmNhLSjedeKtSXyTK4thfmK4wFeA35MEKTQ9WRIkX/hZ4LPK2tWgZB929kyvHBb2Sc81zR8JGMKl7JNxeLHg/Era7q2SZFx4JjfLhl/UbPcOxQRFWx4XFLMQW56xdsAfABqOk3W8Ekf8lSm4T+kbJ+ki2W17jq7duFnHFVpcSIxPtIyOkrEPL+Gh5xU1mPeKj7dYn4AsInLD2NnPLcIPqNnM6PeQxcRGyg7D7xjHDDWQaCv4imXNbqOcQILWElOd67QNK3evSBVB53MtD3XMh0ukT97JNNe1Xh1lHGwDiiPh4iKivNwj3131agFA1ii4gbANMdv1JJGwVTE4z+9aFtYGr3+F2ffuAY+2TFL2iFG12FOS6+eBqkS9T924bZl8z5NZO6zzxUlFdU/xZtaZIP9YoY8qn0ZQGr8kawUcM8Z8IISoMGCUba8kl5ixrmVwy9LGUpFcGP+kqAxj99o+j7PPeZdGejs3adHzg6vi56NhswjxAezqubZ+W6Njbu4KzT8c1StjOOyUsw64s0RTlqxvCX6sPW85E+p/r8EUL2nfgRbI71Jw9bGcP2dZ7XKCU0JY91IsVQ7mIPkQfnCTwDDXPH8N8Ma8lgygTR2howCDqpfMdo5RZIzHDBZJO/c8IDpDO9sQx+769YRZyG56RnPWCo636aTpreUw2AdvmnnTukthUbuprSUHKJKZRCuLJ/D8VpJ6Q/8MHePMv \ No newline at end of file diff --git a/documentation/Software Guidebook/C4 Diagrammen/Component diagram front-end.xml b/documentation/Software Guidebook/C4 Diagrammen/Component diagram front-end.xml new file mode 100644 index 0000000..39c4079 --- /dev/null +++ b/documentation/Software Guidebook/C4 Diagrammen/Component diagram front-end.xml @@ -0,0 +1 @@ +7V1dd5s4E/41ubQP4kOIyyZN055t2rxJ3u72qgcbxabFxgs4sfvrV2BkA5IJdrA11M6eszVCgNB8PRrNDBfG1WRxE7mz8W3o0eBC17zFhfH+QtdNhAn7J21Zrlp0DZurllHke6s2tGl48H/TvFHLW+e+R+NSxyQMg8SflRuH4XRKh0mpzY2i8KXc7SkMyk+duSMqNDwM3UBs/dv3kvGq1dA0bXPiI/VHY/5ozM9MXN47b4jHrhe+FJqM6wvjKgrDZPVrsriiQTp9fGJW133YcnY9sohOkyYX2Msg9B/pi/lr9BzNPj5488WPHuLDfXaDef7O+XCTJZ+EKJxPPZreRrswLl/GfkIfZu4wPfvC6M7axskkYEeI/fTceJz1TQ/EMebDfqZRQheFpnzMNzSc0CRasi75WX4F5yCcD/hlQw7dsYxV47hICo3kXd2cCUbrm2+mif3IZ2qXWROmiHqMbfLDMErG4SicusH1pvWyPImFCaMLP/knbe5b+dH3wpn3i/yK7GCZH/ykSbLMpcWdJyFr2jz1cxjOXpv/OJxHw3zs19efvv78+8dPalnG08cbH93fXPVQLrmJG41oUtOR3zGdgVp6RjRwE/+5LFitk0bk3tMljW2AIo2hmjRspqPlP7xbevB9c4f0cHNZdgSQpAiZoGhqgaGp1lmaGrDE1NIFRHBDB9Hc/5UIxGY2PCnTMKKx/9sdZB3SeZ6F/jTJhmhdXljvWYsb+KMpaxiyGaURa0ixgM8g17v8xMT3vIxRAndAg0t3+GuUscxVGIRR9lzjKfuTkqeOTQXUsQaO+YhL0EyGRrQ+sXAFkVj5cWPy5He/S6dmc+te5Yrw6SmmiUDO9ZgaUbiWM0uYDwcpJQcR+zVKf73zvP/NaZz44bT/M16IHRhB76k7TLJJnMzCaUoCRmGxY5VrXkGQbjxbQfknf5GqiTZQJDLsEtFMU4SRyNIkKNJqAUTWAqcCFZgiGt9Tbz6kEZvzLVPOGJpdlXc7zIQ/+UFQkDbPosQzWXucROEvWjhD9IGBcTskwmUKYUdCIU1GIe1QFEJIuXmLmfVI3qULWdYwZTLG2z746bscyZQZWkNTZtqwTJnZaVNWz5RtGDNNs0pC1yNmK7aM6PLbHt62GTJ/hsS23blx/BJG3o62Tb0lM4ij0JLJF3a2ajW5WQVsgP/3Eu6XrwLo1KsoV9ZyXNVqiKpVPsnKVgm14y6il9lsD/DSMlRxKXkayqAKHhI6kOvZ3X2SFTRJdPVYRVfu+QKCVRq7UjjRgEgU0pU7yDpHQVi+Z4sjsz8JbnKufDvc7Gl9G6MK4DTehjeX0gsOCC+buE4+TZ+ZEft/nBnCbqFLrINDlxirVosFlDgM2LrBH5aAIjqKVjT59nfHgKIpurniZO6x91cOFpki1IdSsOjhAbZa8msh4pB+WeuBwIvKF20w0IZpNkYbFijBQjo5U3BHCmJYeBH/iXiRI6RW8KLDicvx4hv32o6NF03RAy0au880jsPprTt1R3TCprdzqJGQMpUAoEZDuXLsrk+S7+J0DmragrDN2SJMPc4kQyrHmQNimZbWjhCaqCyExwWZUv4w/wQhVLfks9XtDWSXshlwl4UOOcIQrSp3i5OyWxxp1ZDkygVM/Wu1V7Afq0G0apRtce8iqJjgnnql8UTxlsWp7Qy0lpRGdSfjuFEXcqUByd+jyALbkiVNFyywLcLdf/Mgss9+3FPv8qHIs6gtkyoH24bblssHI0epy0fOKwAEaz9r3FTgXhPc/QVyHXfdNYlcD/ztYYYt2zcr/U8miTj7S68Ip0mhffXXjoRilRv4cr5x1EunarO33pIHLGVa8tfD/bev5pRe45vv7u8vn758O1TWkX6otKPKtEmpVaSB9KVbWWMIawKsOX28+SvLqaNXxG/FUvktalYalmOUboQMB0kew2+7enfhtnssQKQzJ64/Nn5fiTJ+uH28axboDcRL3Nj3W8lf3SpaNSGsxLLKPPJWF/ER8i/4y5+mo8qyyzEGAIwv91eftvHtaITBeuBniFsUM1uD5w9WnjgPQcysroqZGN54OiHfxNTBSdOBVh3dkibcVWkSgz9OdRPCsXHfJpaNTH31f2iSpnwbs1VBkkfZNhQjYAme4o5/lyKo6pitnfgpE5fXWnDDp+q4sj56Khz5u5YoeKOWbEHnIYRUViSQAwrlCUhHBRR1+g0wnKgb9nkFXJYxeFBC+S7P4UUINQ1bAlbLCil3AkKiDaxQeQTAcwSgeNwbaQoLvCMA/gsAxePeSFNlSUnyBZmYH9v5BRnn01ZWZATZpIRJOrYiQ+LetYgteeG4a8/fNZkFZvE4gvuE6IalOyZbt/GMZDgrt/aCvP6MWnLIQHUUgxDlrLy0nPq1NoK/E1Y77pPcCENmrTIEsMRG552VhsSCoAfNsx5EHXU6orPXcXfsAUFBKt/PbLNwxL5JJfvLqyxKvTYdquW8TDGPkjF1E5ZrP7xZzl/KndxHL0zSmE/kHXVQet1yBL3efScOf6d2nDid9uFIws+3+3A6XJYEYf4tCzieGlN52a1jlzao1Xhdg7x83Oe1pY5sC8EO29NlvurKVKeGZSaI2Ba7kyOJ4nTzpvJ6Qsv+ZKauQMbmNNjZRjFilAtr6E6fsBWOaViaRSzHsC2ROFhGnCbf0Lsy72jEZKDQbWjWUC8c/KSZwcimuWyA0oksUQj/Ow/5iV6c6aR3rAOyZ4vNSS5eOUyhEb8de/LqjtybvWr2/Of9H9LOUJkmyGdt43bfMlrWnA0Y3jtItgtUvYl0DKWHZdIWMKU6DgOPTX1ZwMwv7iSXrWn6K299XM7y1kHoLXnrexoPI3+W4qPVyUvWMI6oP5hPR6tO9ZrJLIhvrsoKSmmjjFI98Yo1aK6rij7ixgrLjYY5EsgcJLv4luuV8uuGhqM23FBXoQPoKnaYq6tdDI+4QSCiviDwZzF9nb6Ctd/XVFXJn5MV6buwwzHpzwublU1Zj1eYLPADkbDDISxXQ/L/8YVW3gD8GxdrJbCAv/J1GyDa8I8sQaGNcn82JNoAkxvlvmBAtDFhBTxzMT412tSlGkEhjfJ4Z0CkQbB8gPz7vwV0//Hx8S5z6WXVDlWTDoSHt/GH7XBTMLEu+WNZPPIARI2f2teX7LE09rkQmb/jxzCcDPwp5W7mWO5n3uaHAbArU/VNIiRzFUtX+Phg2zLKYQqIitMKSwBll+5acbqaRAug4rR8R1050joGe6niGpHKBi7XPrKdiuJoWjcQmU6Fw9bK6pVgmp1Z2ajUOuRj3jq06gVQmZ8r7dZNYer17DEeTu+wjBNKJ9IN1yt2L5dZTHmCw/ViFtE4Tsdn9hHum2KX23A6Ct9fpl2MPuqnbuZt9rajFthwmvrYD2aBMaTiA+pgM9ZFXVr7XT0oiyJI+SywyCfvCMsVxMddGxL2gVJvwHRu9wLBiF2txw8gFAwr31KAITLwK+vXjvsks8B0zXD6BuQkB1u5BxWGdDkNpcuGtafHx11rkHhLPHOn5Y8pzqJwFLmT3j2N50EpcpmNpti9c7YsBevwbJmtn6WtIERds2W2uEQ/rY90M4howvtik608sAGGVKn7DuLbyCfuAZ2YVJkaAShVkFLOFUpV45K/wKTq7GnajXywqr7ZTWr63kUMfPtD2j10bhCI6PzkdrdrJaFzOEJSZ+N0PE2WCdzTxBMNTly6mkda6qCki5wdhbuRz4FFvrPnaSfy2bACnZ3ztn/GxR3dBCPiJtiJuTiwo8NzcZCz4zCbhqYFRgmsNTI5r9d2I1/rNm2vEE7dNquLf2zWh2TqhBj1lxwmJtMBAHqTvb6n0DHOPEwFwKNwJkJvv4TYR2BmIiu30CRW4GEFVe5WwQKTuCZOoFteSGxXtQoAL6Rz3ncpGqvOQX3RcX9CXkhiGLC9kM55W6yI5LuSwabbxCixEtQsHiJuQpxq5cNsza8XKh+WUZL6ovqWmKG+tYDvKSoJ1LoJXuem25a5V276zumClnHY7L9jJss7AApmAWDLxj58Auujb4YuqJu8IEYEoyAGINJxoNhUo2TVxh19H5VyTAlmukTggV2TfJnSkyT58sU5jUJJRlqU3kKaAvxa3q1YuL6MGbbX+m1evl6GZsqM7bnxeF3oswVoUqmViNiCRQAjOg/oK4KRaoL8PspBS/56uP/21ZzSa3zz3f395dOXbz0RirxW9jWlTC+f5JQv8nneWvl15aZpkESOZfx16/pBTKPnUs1gmedHXrt2l+rC+73YGj5fL1hvprAyh5OY0r7TsNsa4utVd9t5XkqlhAlQ3w/rH3SYdHtJRW5BTLfKpKVXkJo8296UrBB44w5SyQ6jMJ27jXpnbzm+Db201Pb1fw== \ No newline at end of file diff --git a/documentation/Software Guidebook/C4 Diagrammen/Container diagram.xml b/documentation/Software Guidebook/C4 Diagrammen/Container diagram.xml new file mode 100644 index 0000000..fde9c0e --- /dev/null +++ b/documentation/Software Guidebook/C4 Diagrammen/Container diagram.xml @@ -0,0 +1 @@ +7VtLc5s6FP41XsbD049lHbvtdJo207T33ixlkDEJRlTIjp1ff4+EhBFgsBMntZOySNDRA3H0nTfu2JeL9SeKkvkV8XHUsQx/3bHHHcsyDceCf5yyyShuz8kIAQ19OWhLuAkfsZopqcvQx6k2kBESsTDRiR6JY+wxjYYoJQ/6sBmJ9KcmKMAVwo2Hoir139Bn84w6sPpb+mccBnP1ZLM3zHoWSA2Wb5LOkU8eCiR70rEvKSEsu1usL3HEmaf4ks37uKM33xjFMdtnwuDX49X41hjRXwj9GrAowJ8eLuQqKxQt5QvLzbKN4kBAyTLp2CP4F/uYL2ZAK0JTHI2Qdx8I+iWJCIWumMQwbTQLo6hMIjFTpI5lG+ICOorCIAaaB6+BoXM0Z4sI2ibcVl9R7RdThtd1AEBTte8tjwGcmCwwoxsYJ2c5cpqEpS0P6WF7xqYpeTEvnq86TSRxFeQrb1l/6VxjmpK4MMxzigPgRh6PaJLpHYeuZQimZhNu2NLnb231Inj0yA9XcBvw2447ksu7Y9UNCxZH1EySpCktU1qnTnAsACH3Y/ghhr9YUJHnwelz6hQ/hmzXmuIUI+ThOYgf7F0/OOcbWsgzi/mdpP7cJJI6Jf5GUcc49WiYsJDEWecICHOKw+kyDrJBjYC3CqiQAlLA+hbjHH4P85DhmwT2DYQH0G86OvcXAcC7j/Bg5h0iB4h6Uh3aNjRTRsk9LszseQM8ndVKSaOol0WnKiJSJpxet6ddzn4y0re7dvEyjy8x0JRCc4Cus9t1HawChgW3nz1Kk8zazMI1x8uTtWMZGvLIga8HQOUVsSF7rYGmPi+sCjRcqzs0CuDpV4GyY8gxgbIfMgbtyNDPtwUdPkrnuRapHo64DjJvu8/B0M7Bcqp2zB0YVdb39rFjT+KlWlhjZmYWOJ41rvZ+L4nquEgF8j/AANNK1ttOZUY+4SldhvesxpS5o88/f153LNix8eXm+zfdMmaP3W0HyzrABwdQNgllcxKQGEWTLbUk64Vzx+uQ/cfJXVe2bgs947WcIRob1YiB64VJvHlb7NtOEy017w4ztpHaAi0ZAdJ2t18JSTT4Kd+10atKyZJ6uOFspWvLEA1wozqRGOCsbEQxxRFi4Ur3tY8OyeFORAovpQaQIH3sQqpZjkipaSuYVOukCYr3QnavDtkfKR+FY58vsUkZxosCfLO193Ha6oB+3DfM5e0SHoXCmM8wxAI9tOBKMJ6mib7Puu0XaT8wEr7vygR3o2s3Oqrta/nLNV/L7Zpd84CJusLYn+PH4u5BbvkxnqcIEGYsKWd/4bl5Z6ZUjR+Tm59CVn8vccpSuI0RosKgCM8f3JwyeA0RHQQYz/iIR7INIESHzyOIBQlwFN7dg1nicwmfxngHwTMci3HZYOK1zrumJKBoscCUqFWmeI5Bw3VfmaHfY7ZCcdDM0rKwAG/ThMQpToWaipt5K97Yx49YvSojGbsQypkUZMaSx1qN718xjyVjeKgbfASXpqe7ltawJjQf1oXminh8l8Z9CSdB3d9qDsNhToJ5gk6CMv6tXoLysk/ES7B6FTfhKwaFl4tklgfx5jS8E2oNuFfnipYc0AZPkysZHRugB8JHmcLi55eQMGbiRV1ho2piPy5YoYeiD7JjEfq+AOCOGBQCkJm4ag+5Ef/Pz7pdGN2eafb12FEutfcpy9WvOW+2S9tO19XWNW19CTKbpVyplmCSb/IZyBkcVT/ksq6FA92++2LCXq+S+s066TU0RP+0NEQ1kPgHLKxyoSYXCxSK6gch3D3ikiiqIUJ91GiKmytwBt6WplCZlGdrCjB0xqCkKKyjKAqAn577t2ylOV5eVZi7syOnEYuOajzOMwtFjS9ohbIqgUgJPTk0nawTEDPhkztdCE+dvxFl5XmFeEeFi5nPVAgXixHNrJrreOEQLQ9xYT80xIX4VURKPgKdhFKcB1bQls5dFl2lEcRVL7zHAhf5Lje1IaG23ZYNnX5c1zP+eFxnOjXKuMSot1n9Nod63eDkyt9jkfTRxOtPVr99uZ0zL36bbgEV76z67eyQnXdT/jar+Y1q2Ppe69/7o+NNFsBVSubtlkF3GtPWbIU82dZkhUwOnEiuwqx+09BeQH9TuQiF6aNkLZ2BYWsCr7TpM5MRrlnKWl44r5iMGP6V+mZX6ezEXtn0k80vjfNw+8yySk/OIl2ROCBjXuVd2fyzBOO5axGRrFi5XVjO/ZuSakqmiFpcMfPTVlx/lUSUSOTIrak0j57lWYYswCuCqWiJlND5JXzsUpLhBCr5Ks+x88POWnvWLxq03IY1G7S8klco779M7a7VYj/D9e3vaQSVtTwRK6hi7mM7Nk8Bwhl81rn3MdundcpVXydXsVjVYrNU/5mGMPbREr4QwriD4XE/vHiFDyv6lRM+LW82L/mnmK7EY07aqZ2sYTTokQw/paopfxMGjkE3JO/QU8xS+uKLjLx22eovvnBVsPhxSba/klqT1Pxjz7NzEfOfobxGURCa21/bZipq+5tle/I/ \ No newline at end of file diff --git a/documentation/Software Guidebook/C4 Diagrammen/Context diagram.xml b/documentation/Software Guidebook/C4 Diagrammen/Context diagram.xml new file mode 100644 index 0000000..81a59d0 --- /dev/null +++ b/documentation/Software Guidebook/C4 Diagrammen/Context diagram.xml @@ -0,0 +1 @@ +7Vrbcts2EP0aTZ6k4UWkpMfIt860TtK6U8d9yUDkioRNEgwI3fL1AUBAvFOSrTROEz3YwHIBLHbPLg4oDeyLeHtDURreEh+igWX424F9ObAs0xhb/J+Q7JTEnTq5JKDYV7JCcIe/gB6qpCvsQ1ZRZIREDKdVoUeSBDxWkSFKyaaqtiRRddUUBdAQ3Hkoakrvsc/CXDq1JoX8N8BBqFc23Vn+JEZaWe0kC5FPNiWRfTWwLyghLG/F2wuIhPe0X/Jx1x1P94ZRSNgxA+6N6e2/fxnG+p0bX5L1786n9Wpoqn1kbKd3DD53gOoSykISkARFV4V0Tskq8UFMa/BeyOKIN03ehC1mH4V45KjeQ+nJ5VaNkJ2d7iSM7vJBhmFpwYMUuJarBcVg2auM/gAUx8CAKuEjMLZTUEIrRrio2MgfhKTKqKYHlVMzsqIe9LhtrJCIaACsR09hQbi0tICKzw0QbjXdcQUKEWJ4XcUcUtAN9npFdHlDBfiUYE/zidcoWqmlBpYb8Q3MsxQlvB2I9j9AM7ZaUSYsH8YIR5nIYEKogDX28xxfywFyMDemPL6BKAZbVgUKhQx/QQupICKWEpwwuV1nPnAuuQRFOEi4wOOxEYGdr4EyzPPyrXogDJFojNACojnyngKJywsSESrXtZfy0xro3mQQS8F20FJelMmVDK4EUo0yRtbUVENV5Ruq7tGhVpN/EK4pqZDlMuOYq2Nhb8Pz4THuRMeSSBOKkLqfV0Q/GGYy095yBdNKt8XDAg9ljB0xy7RtlntYCC+kacRRwDDoWRe0vk4DjrkY7wXO/JqKJSHxhT27jAHEAnbFBLg+urmMdoo+srTBZrcHWm3N56mK03bfqfzMM/GSeBLOwmW3CD0JY0KBDCMmAUT48Yk3Scz/RJCllAQUxTF684Yn80UuzEDEQ/5ZU36QiQbjjjUWEAKVXbKUBy7oJ2sCXG/ETUExL6PzZJGlh11flr0nwq4nCQUQ+wDIUbHyQTpULunnm5NrByBWFaHaEOpL7Q1CYmiS/8tnwMkaSxtFxVLKgkCALGaiGUvv5NrI40mY44gnty/++bLDH8ei+MihC7rC0rG5vfv1pVv1RB4vO0w7SM4rnDc64BMubkT5mLDfKUfJuN8ALFmLD+V+NAxCwL62WdpIYAmJHJHKCJTx8RyzGyCu1f9NyANzlyJ5pG44UaweBihLc+a2xFvBKrrP5UZh7izApjGrVF+LMwnZ3xQkznIVMwtLBG4vPP/pa317qrVnV61U61hetKdk+85DsVIPGXs+n5o1+dSndBhi8/b953fX11cfnx4/re7/HFrn5lO1Q1ahx3KMCnomNUzkdqpBBSzeUop2JTXFajqXcWvLDJ0qga+p22OnR5038vXPyglMu0EK2ulhXop/VOJnnY/4jWdWlfjZL+N92pxRNfgT3f/2vHB2gBeek+Xd5md3BnQtjqbeo6jJphqs7YXE1ZkLeHXyxKPpXD+h7EsozUG4yuiEZb//Aey41drmTJzGAWxOWw7gvfAlB3Dr4WG24LjmKFFJ0sax21FoEpKIMrTEUVQX8WCUypEhP221rRSEEytQS3i6ydCsfhXV7KgcDNtpCYZzRDAuxh84PElSUvPGPdEii0cBN847hV+rFcXHa41iTXQLjJcetujzbFV2VG5yBwb15VT/0Ks659bXh8qlYAFfcOceZBgjnpYhiXxuezXu43coViFPREtJ/96lSrog/k5LLyHzKE4ZJkn+cM4FIQW8WCVBrtSbHFYJQSqZSnlR5IOA6oFycny68NzwEUyX3ik5g6inOKxti/OGUfIEpZGuN4XFsi+jypnTXSma6aRL2XRkVz7Nq0VrMs3MkVv+TM6fWryrsuuEutikeI26yGfBaQaHY984Sp5bSevQUCE3rVOg8h9iQ3N0e9Ry0Llua+TL4OhQOSc4jkOD0wz+d/tCYH/hfCg/67h9nvbWv3aFPHht7buOHvwaYNyOmbO/9W810m2k983+vdbrvK89kwR1w7nnumZOdaqd6T191W79tWctnc9yWWvd76QR7Z+E5I4bHHfy+jmueof/uinu/l38j81wp78Ybq1M/DQMt+0N1i+GeyI2/jcMV/+06EwUt6Cr1mvnq9Mfga+azfd0PxNhNfu/YBgao7E7G1e4zvhljPUlpJR3i1+w5erFDwHtq68= \ No newline at end of file diff --git a/documentation/Software Guidebook/C4 Diagrammen/README.md b/documentation/Software Guidebook/C4 Diagrammen/README.md new file mode 100644 index 0000000..bab5302 --- /dev/null +++ b/documentation/Software Guidebook/C4 Diagrammen/README.md @@ -0,0 +1,6 @@ +# C4 Diagrammen +## Wil je de diagrammen graag aanpassen? +Dat kan! De xml bestanden die in deze directory staan kan je importeren in [draw.io](https://www.draw.io/). [Hier](https://about.draw.io/draw-io-training-exercise-10-export-and-import/) is een korte tutorial te vinden hoe je binnen draw.io kan importeren en exporteren. + +### C4 Model extentie +In de diagrammen is een extentie gebruikt op draw.io, deze extentie is [hier](https://github.com/tobiashochguertel/c4-draw.io) te vinden. In deze repo kan je ook zien hoe je deze extentie kan instaleren diff --git a/documentation/Software Guidebook/ERD/ERD PROJECT.gliffy b/documentation/Software Guidebook/ERD/ERD PROJECT.gliffy new file mode 100644 index 0000000..eaaf853 --- /dev/null +++ b/documentation/Software Guidebook/ERD/ERD PROJECT.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.1","metadata":{"title":"untitled","revision":0,"exportBorder":false},"embeddedResources":{"index":0,"resources":[]},"stage":{"objects":[{"x":605,"y":72.5,"rotation":0,"id":97,"uid":"com.gliffy.shape.basic.basic_v1.default.text","width":150,"height":14,"lockAspectRatio":false,"lockShape":false,"order":97,"graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"

ObjectId verwijzing

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null,"linkMap":[]},{"x":479,"y":78.5,"rotation":0,"id":96,"uid":"com.gliffy.shape.erd.erd_v1.default.many","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":96,"graphic":{"type":"Line","Line":{"strokeWidth":1,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":14,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[0,0],[50,0],[50,0],[100,0]],"lockSegments":{}}},"children":null,"linkMap":[]},{"x":198,"y":194,"rotation":0,"id":94,"uid":"com.gliffy.shape.erd.erd_v1.default.many","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":94,"graphic":{"type":"Line","Line":{"strokeWidth":1,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":14,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[0.9999999999999716,1.2548339959390375],[33.669601619888994,1.2548339959390375],[66.33920323977804,1.2548339959390375],[99.00880485966707,1.2548339959390375]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":10,"px":0.9999999999999998,"py":0.7071067811865475}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":81,"px":0,"py":0.7071067811865475}}},"linkMap":[]},{"x":491,"y":255,"rotation":0,"id":92,"uid":"com.gliffy.shape.erd.erd_v1.default.one_many","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":92,"graphic":{"type":"Line","Line":{"strokeWidth":1,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":10,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[-133,-35],[-133,-15],[-133,5],[-133,25]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":81,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":86,"px":0.5,"py":0}}},"linkMap":[]},{"x":500,"y":138,"rotation":0,"id":91,"uid":"com.gliffy.shape.erd.erd_v1.default.one_many","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":91,"graphic":{"type":"Line","Line":{"strokeWidth":1,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":10,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[-142,-12],[-142,-5],[-142,-5],[-142,2]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":69,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":81,"px":0.5,"py":0}}},"linkMap":[]},{"x":298,"y":280,"rotation":0,"id":86,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","width":120,"height":78,"lockAspectRatio":false,"lockShape":false,"order":42,"graphic":null,"children":[{"x":0,"y":0,"rotation":0,"id":87,"uid":null,"width":120,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":88,"uid":null,"width":120,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

Answers

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":88,"magnitude":1}],"growParent":true,"padding":0}}]}},{"x":0,"y":18,"rotation":0,"id":89,"uid":null,"width":120,"height":60,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":90,"uid":null,"width":120,"height":60,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

number: Number\n

answer: [String]\n

correct: Boolean\n

score: Number

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":86,"magnitude":1},{"id":87,"magnitude":-1}],"minHeight":20,"growParent":false,"padding":0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":87,"px":0,"py":1}}]}}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"id":87,"magnitude":1},{"id":90,"magnitude":1}],"growParent":false,"padding":0}}]},"linkMap":[]},{"x":297,"y":140,"rotation":0,"id":81,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","width":122,"height":80,"lockAspectRatio":false,"lockShape":false,"order":33,"graphic":null,"children":[{"x":0,"y":0,"rotation":0,"id":82,"uid":null,"width":122,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":83,"uid":null,"width":122,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

LessonResults

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":83,"magnitude":1}],"growParent":true,"padding":0}}]}},{"x":0,"y":18,"rotation":0,"id":84,"uid":null,"width":122,"height":62,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":85,"uid":null,"width":122,"height":60,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

lessonId: ObjectId\n

date: Date\n

score: Number\n

completed: Boolean

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":81,"magnitude":1},{"id":82,"magnitude":-1}],"minHeight":20,"growParent":false,"padding":0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":82,"px":0,"py":1}}]}}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"id":82,"magnitude":1},{"id":85,"magnitude":1}],"growParent":false,"padding":0}}]},"linkMap":[]},{"x":298,"y":20,"rotation":0,"id":69,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","width":120,"height":106,"lockAspectRatio":false,"lockShape":false,"order":27,"graphic":null,"children":[{"x":0,"y":0,"rotation":0,"id":70,"uid":null,"width":120,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":71,"uid":null,"width":120,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

Users

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":71,"magnitude":1}],"growParent":true,"padding":0}}]}},{"x":0,"y":18,"rotation":0,"id":72,"uid":null,"width":120,"height":88,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":73,"uid":null,"width":120,"height":88,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

name : String\n

e-mail : String\n

password : String\n

mailToken: String\n

isAdmin : boolean\n

score: Number

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":69,"magnitude":1},{"id":70,"magnitude":-1}],"minHeight":20,"growParent":false,"padding":0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":70,"px":0,"py":1}}]}}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"id":70,"magnitude":1},{"id":73,"magnitude":1}],"growParent":false,"padding":0}}]},"linkMap":[]},{"x":465,"y":181.5,"rotation":0,"id":66,"uid":"com.gliffy.shape.basic.basic_v1.default.text","width":300,"height":14,"lockAspectRatio":false,"lockShape":false,"order":26,"graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"

* Iedere collectie heeft een _id van het type ObjectId

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null,"linkMap":[]},{"x":615,"y":132.5,"rotation":0,"id":64,"uid":"com.gliffy.shape.basic.basic_v1.default.text","width":150,"height":14,"lockAspectRatio":false,"lockShape":false,"order":25,"graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"

Een collectie

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null,"linkMap":[]},{"x":467,"y":102.5,"rotation":0,"id":59,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","width":120,"height":60.00000000000001,"lockAspectRatio":false,"lockShape":false,"order":20,"graphic":null,"children":[{"x":0,"y":0,"rotation":0,"id":60,"uid":null,"width":120,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":61,"uid":null,"width":120,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

Collectie

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":61,"magnitude":1}],"growParent":true,"padding":0}}]}},{"x":0,"y":18,"rotation":0,"id":62,"uid":null,"width":120,"height":42.00000000000001,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":63,"uid":null,"width":120,"height":32,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

_id*\n

Attributen

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":59,"magnitude":1},{"id":60,"magnitude":-1}],"minHeight":20,"growParent":false,"padding":0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":60,"px":0,"py":1}}]}}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"id":60,"magnitude":1},{"id":63,"magnitude":1}],"growParent":false,"padding":0}}]},"linkMap":[]},{"x":593,"y":30,"rotation":0,"id":50,"uid":"com.gliffy.shape.basic.basic_v1.default.text","width":170,"height":14,"lockAspectRatio":false,"lockShape":false,"order":19,"graphic":{"type":"Text","Text":{"tid":null,"valign":"middle","overflow":"none","vposition":"none","hposition":"none","html":"

Embedded array of documents

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null,"linkMap":[]},{"x":473,"y":43,"rotation":0,"id":48,"uid":"com.gliffy.shape.erd.erd_v1.default.one_many","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":18,"graphic":{"type":"Line","Line":{"strokeWidth":1,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":10,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[0,0],[50,0],[50,0],[100,0]],"lockSegments":{}}},"children":null,"linkMap":[]},{"x":457,"y":20,"rotation":0,"id":46,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","width":314,"height":190,"lockAspectRatio":false,"lockShape":false,"order":17,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":0,"shadowY":0,"opacity":1}},"children":[],"linkMap":[]},{"x":290,"y":220,"rotation":0,"id":21,"uid":"com.gliffy.shape.erd.erd_v1.default.one_many","width":100,"height":100,"lockAspectRatio":false,"lockShape":false,"order":16,"graphic":{"type":"Line","Line":{"strokeWidth":1,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":10,"startArrowRotation":"auto","endArrowRotation":"auto","ortho":true,"interpolationType":"linear","cornerRadius":null,"controlPath":[[-185.5,-6],[-185.5,16],[-185.5,38],[-185.5,60]],"lockSegments":{}}},"children":null,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":10,"px":0.5,"py":1}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":15,"px":0.5,"py":0}}},"linkMap":[]},{"x":24.5,"y":280,"rotation":0,"id":15,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","width":160,"height":78,"lockAspectRatio":false,"lockShape":false,"order":10,"graphic":null,"children":[{"x":0,"y":0,"rotation":0,"id":16,"uid":null,"width":160,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":17,"uid":null,"width":160,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

CodeCards

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":17,"magnitude":1}],"growParent":true,"padding":0}}]}},{"x":0,"y":18,"rotation":0,"id":18,"uid":null,"width":160,"height":60,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":19,"uid":null,"width":160,"height":46,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

number : Number\n

question : String\n

parts : [String]

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":15,"magnitude":1},{"id":16,"magnitude":-1}],"minHeight":20,"growParent":false,"padding":0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":16,"px":0,"py":1}}]}}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"id":16,"magnitude":1},{"id":19,"magnitude":1}],"growParent":false,"padding":0}}]},"linkMap":[]},{"x":10,"y":150,"rotation":0,"id":10,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","width":189,"height":64,"lockAspectRatio":false,"lockShape":false,"order":5,"graphic":null,"children":[{"x":0,"y":0,"rotation":0,"id":11,"uid":null,"width":189,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":12,"uid":null,"width":189,"height":18,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

Lessons

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":12,"magnitude":1}],"growParent":true,"padding":0}}]}},{"x":0,"y":18,"rotation":0,"id":13,"uid":null,"width":189,"height":46,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dropShadow":false,"state":0,"shadowX":4,"shadowY":4,"opacity":1}},"children":[{"x":0,"y":0,"rotation":0,"id":14,"uid":null,"width":189,"height":46,"lockAspectRatio":false,"lockShape":false,"order":"auto","graphic":{"type":"Text","Text":{"tid":null,"valign":"top","overflow":"none","vposition":"none","hposition":"none","html":"

name : String\n

description : String\n

programmingLanguage: String

","paddingLeft":2,"paddingRight":2,"paddingBottom":2,"paddingTop":2}},"children":null}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"id":10,"magnitude":1},{"id":11,"magnitude":-1}],"minHeight":20,"growParent":false,"padding":0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":11,"px":0,"py":1}}]}}],"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"id":11,"magnitude":1},{"id":14,"magnitude":1}],"growParent":false,"padding":0}}]},"linkMap":[]}],"background":"#FFFFFF","width":771,"height":360,"maxWidth":5000,"maxHeight":5000,"nodeIndex":106,"autoFit":true,"exportBorder":false,"gridOn":true,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":"LETTER","printShrinkToFit":false,"printPortrait":true,"shapeStyles":{"com.gliffy.shape.basic.basic_v1.default":{"fill":"#FFFFFF","stroke":"#333333","strokeWidth":2}},"lineStyles":{},"textStyles":{},"themeData":null}} \ No newline at end of file diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V1.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V1.jpg new file mode 100644 index 0000000..4148208 Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V1.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V2.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V2.jpg new file mode 100644 index 0000000..813b932 Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V2.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.0.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.0.jpg new file mode 100644 index 0000000..0b19408 Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.0.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.1.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.1.jpg new file mode 100644 index 0000000..ca1298b Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.1.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.2.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.2.jpg new file mode 100644 index 0000000..82ed267 Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.2.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.3.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.3.jpg new file mode 100644 index 0000000..fee624c Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.3.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.4.jpg b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.4.jpg new file mode 100644 index 0000000..144104a Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 1/ERD image V3.4.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 2/ERD image V4.0.jpg b/documentation/Software Guidebook/ERD/Sprint 2/ERD image V4.0.jpg new file mode 100644 index 0000000..785bd3e Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 2/ERD image V4.0.jpg differ diff --git a/documentation/Software Guidebook/ERD/Sprint 2/ERD image V4.1.jpg b/documentation/Software Guidebook/ERD/Sprint 2/ERD image V4.1.jpg new file mode 100644 index 0000000..6d3ec0e Binary files /dev/null and b/documentation/Software Guidebook/ERD/Sprint 2/ERD image V4.1.jpg differ diff --git a/documentation/Software Guidebook/README.md b/documentation/Software Guidebook/README.md new file mode 100644 index 0000000..fdf1d3d --- /dev/null +++ b/documentation/Software Guidebook/README.md @@ -0,0 +1,309 @@ +# Introductie +Dit software guidebook zal inzicht geven op vele aspecten van de volledige Programmero applicatie. De hoofdstukken zijn opgedeeld over verschillende bestanden in de map `documentation/Software Guidebook/chapters` aan de hand van een [website](https://leanpub.com/techtribesje/read#introduction) en de inhoudsopgave van het boek dat op de website wordt aangegeven. Daarbij heeft de projectgroep het hoofdstuk "REST endpoints" toegevoegd omdat we deze documentatie belangrijk vinden en het een toevoeging heeft voor het software guidebook en de gebruikers en administrators van Programmero. Het hoofdstuk "Implementatie" is weggehaald in het SGB omdat deze is gedocumenteerd in de `Readme.md` van dit project. + +Om een duidelijke focus te leggen op wat het software guidebook te documenteren heeft zullen we dit samenvatten in een paar punten: +- De context, probleem en oplossing van de applicatie +- De requirements, limitaties en principes van de applicatie +- Een overzicht van de mogelijkheden die de backend van de applicatie bied in de format van REST endpoints +- De software architectuur van de applicatie op beide hoog niveau en in detail +- Infrastructuur van de applicatie +- Onderhoud en ondersteuning na implementatie van de applicatie + +# Context +In dit hoofdstuk zal de context van de applicatie Programmero worden gedocumenteerd. Dit hoofdstuk is erg belangrijk om de intentie en motivatie van het probleem in te zien. In het volgende hoofdstuk zal worden ingegaan hoe dit probleem in zijn geheel wordt opgelost met de applicatie. + +## Probleemstelling +Programmero is een applicatie die docenten van de HAN (hogeschool van Arnhem -en Nijmegen) een nieuwe manier van het vak SPD geven zou moeten aanbieden. Docenten hebben hierbij aangegeven dat bepaalde studenten die het vak SPD volgen, soms niet goed kunnen meegaan in de lessen. De docenten van de HAN hebben onderzoek gedaan naar lesmethoden die anders zijn om zo de studenten te helpen die moeite hebben om het huidige lesprogramma te volgen. + +De huidige lesmethode valt onder het "whole-word" principe. Hierbij worden hele woorden gebruikt om programmeertalen uit te leggen en te oefenen. Hierbij wilt de productowner een applicatie hebben die een "phonix" principe aanpakt. Het "phonix" principe bied een lesstijl aan die een taal en grammatica opdelen in kleine stukken ophakt en daarin vragen kan neerzetten om mee te oefenen. Hiermee kan de productowner deze lesmethode aan zijn studenten aanbieden als ze moeite hebben met het "whole-word" principe in de lessen? + +## Uitwerking +Het doel van de applicatie is: +- Een unieke applicatie neer te zetten voor studenten om nieuwe leermethodiek te promoten +- Vragen en lesprogramma's aanbied voor studenten die in deze leeromgeving moeten oefenen met een programmeertaal +- Docenten de macht geven om lesprogramma's en vragen te beheren + +Het product zal bestaan uit twee onderdelen: de backend die alle aanvragen behandeld en die toegang heeft tot de database. En de React frontend die kan worden bezocht door gebruikers en door studenten en docenten kan worden gebruikt. Deze twee onderdelen vormen samen de applicatie. + +## Gebruikers +Programmero heeft drie typen gebruikers: +- Anonieme gebruiker +- Student +- Docent + +Elk van deze gebruikers zal nog worden toegelicht onder hoofdstuk 3: functionele overview + +# Functioneel overzicht +Dit hoofdstuk zal ingaan op alle inhoud die in de applicatie van Programmero zit. Dit gaat over informatie die de applicatie geeft, maar ook functionaliteit die de applicatie aanbied om zijn doel te bereiken. + +## Lesprogramma's en codekaarten +Programmero bestaat uit: +- Lesprogramma - een lesprogramma is een lijst van codekaarten die: + - De programmeertaal en structuur van een lesprogramma definiëren + - Een oefening neerzet voor studenten +- Codekaart - een entiteit die uit de volgende onderdelen bestaat: + - Een vraag gekoppeld aan de programmeertaal van het lesprogramma + - Een antwoord die in phonix stijl is geformuleerd +- Score - Een visuele representatie van de score van een student of lesprogramma resultaten +- Student - Gebruiker die oefeningen kan doen op lesprogramma's die door een docent zijn gemaakt en daardoor punten kan scoren +- Docent - Gebruiker die lesprogramma's en codekaarten binnen een lesprogramma beheert + +Binnen Programmero zijn lesprogramma's en codekaarten de kern van de applicatie. Een lesprogramma is een lijst van codekaarten die een docent kan aanmaken. Een student kan aan de hand van een lesprogramma vervolgens codekaarten invullen om het lesprogramma te voltooien en zo een oefening te voldoen. Daarbij kan de student bij elke oefening een score behalen. + +## Gebruikers +#### Anonieme gebruikers +Anonieme gebruikers representeren alle gebruikers die geen account of inloggegevens hebben. Anonieme gebruikers kunnen niet in de applicatie komen en kunnen alleen het inlogscherm zien. + +#### Student gebruiker +Student gebruikers representeren alle gebruikers die een account hebben met studentfunctionaliteit. Dit houdt in dat ze oefeningen kunnen maken van de lesprogramma's die in de applicatie staan en hier een score mee kunnen behalen. +Een student kan aan de hand van informatie die een leraar heeft een email ontvangen om een account te activeren. + +#### Docent gebruiker +Docent gebruikers representeren gebruikers die een administrator account hebben. Hierbij kunnen docenten lesprogramma's en codekaarten aanmaken om ervoor te zorgen dat studenten kunnen oefenen aan deze lesprogrammas's. Ook kan een leraar een gebruiker toevoegen aan het systeem door een email en naam in te voeren. Vervolgens kan deze gebruiker een eigen wachtwoord invoeren. + +## Gamificatie +In de applicatie is een vorm van gamificatie ingebouwd. Dit is een een basisvorm geïmplementeerd door een score per student, lesprogramma en vraag te berekenen. Deze scores kunnen vergeleken worden met studenten zodat er een competitieve omgeving kan worden gecreëerd. In de toekomst kan er ook op deze basis een competitieve vorm worden gemaakt waarin een student een andere student kan uitdagen in een in-app competitieve omgeving. Deze functionaliteit is uitgeschreven in [userstory 4](https://github.com/HANICA-DWA/sep2018-project-aardvark/issues/5). + +# REST API reference + +Dit hoofdstuk zal alle REST endpoints documenteren. Dit is zodat er een duidelijk overzicht is voor developers om te zien waar ze informatie kunnen halen, geven, aanpassen en verwijderen. De REST backend server gebruikt Node.JS en heeft MongoDB en ExpressJS geïnstalleerd staan. + +Alle inkomende aanvragen (requests) moeten met JSON worden aangevraagd. Hierbij moet er gebruik gemaakt worden van MIME-type "application/json" in de HTTP request header. + +Het returntype van elk endpoint in de server is JSON. Hierbij zal het MIME-type "application/json" in de HTTP header worden meegegeven. + +## Authenticatie + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /auth/:mailToken/check | - | Checkt of een mailtoken van een gebruiker bestaat | HTTP 200: String
HTTP 400: Object | +| POST | /auth/login | `email: String`
`password: String` | Logt een gebruiker in op basis van gegeven email en password | HTTP 200: Webtoken
HTTP 422: Object
HTTP 500: Object | +| POST | /auth/secret | `mailToken: String`
`pass: String`
`passrepeat: String` | Valideert wachtwoord en zet een wachtwoord voor de gebruiker met aangegeven mailtoken | HTTP 200: No content
HTTP 400: Object | + +## Lesprogramma's + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /lessons | - | Geeft alle lesprogramma's terug | HTTP 200: Array
HTTP 401: Object
HTTP 500: Object | +| GET | /lessons/:lessonId | - | Geeft een specifiek lesprogramma terug op basis van het meegegeven ID | HTTP 200: Object
HTTP 401: Object
HTTP 422: Object
HTTP 404: Object
HTTP 500: Object | +| POST | /lessons | `name: String`
`description: String`
`programmingLanguage: String` | Maakt een nieuw lesprogramma aan de hand van meegegeven POST gegevens | HTTP 201: Object
HTTP 401: Object
HTTP 422: Object
HTTP 500: Object | +| PUT | /lessons/:lessonId | `name: String`
`description: String`
`programmingLanguage: String` | Past een bestaand lesprogramma aan aan de hand van meegegeven PUT gegevens | HTTP 200: Object
HTTP 401: Object
HTTP 422: Object
HTTP 500: Object | +| DELETE | /lessons/:lessonId | - | Verwijderd een specifiek lesprogramma op basis van het meegegeven ID | HTTP 204: No Content
HTTP 401: Object
HTTP 422: Object
HTTP 500: Object | + +## Codekaarten + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /lessons/:lessonId/codecards | - | Geeft alle codekaarten van een specifiek lesprogramma op basis van meegegeven ID | HTTP 200: Array
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| GET | /lessons/:lessonId/codecards/:codecardId | - | Geeft een specifieke codekaart in een lesprogramma terug op basis van meegegeven IDs | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| GET | /lessons/:lessonId/codecards/student | - | Geeft een codekaart in een lesprogramma terug op basis van meegegeven ID en lessonresult dat de student heeft Zorgt ervoor dat het antwoord door elkaar gehusseld is | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 409: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /lessons/:lessonId/codecards | `question: String`
`answer: [String]` | Maakt een nieuwe codekaart in een lesprogramma aan de hand van meegegeven POST gegevens en ID | HTTP 201: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| PUT | /lessons/:lessonId/codecards/:codecardId | `question: String`
`answer: [String]` | Past een bestaande codekaart in een lesprogramma aan aan de hand van meegegeven PUT gegevens en IDs | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| DELETE | /lessons/:lessonId/codecards/:codecardId | - | Verwijderd een specifieke codekaart in een lesprogramma op basis van het meegegeven ID | HTTP 204: No content
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | + +## Oefenresultaten + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /users/lessonresult/:lessonId/end | - | Haalt oefenresultaten op van laatste oefening die in een les is gemaakt op basis van meegegeven ID | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /users/lessonresult/:lessonId | - | Maakt een nieuwe lessonresult, zodat een student met een oefening kan beginnen | HTTP 201: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /users/lessonresult/:lessonId/answer | `answer: [String]`
`index: Int` | Voegt een nieuw antwoord toe aan een lessonresult | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | + + +## Gebruikers + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /users/score | - | Vraagt de totaalscore van een gebruiker op | HTTP 200: Number
HTTP 401: Object
HTTP 500: Object | +| GET | /users/score/:lessonid | - | Vraagt de score van een specifieke les van een gebruiker op | HTTP 200: Number
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /users | `name: String`
`email: String`
`isAdmin: Boolean` | Maakt een nieuwe user aan en stuurt een mail naar naar de desbetreffende user | HTTP 201: No content
HTTP 400: Object
HTTP 422: Object
HTTP 500: Object | + +# Data structure +In dit hoofdstuk van het softwareguidebook wordt het ERD en de datastore van Redux beschreven. + +#### ERD + +![](images/erd.jpg) + +In het bovenstaande diagram wordt het entity relationship model getoond. In dit diagram wordt alle informatie die in de database wordt opgeslagen gemodeleerd. We hebben dit database model gemodelleerd aan de hand van document georienteerde manier (MongoDB). + +In dit model zijn er twee collecties gemodelleerd met schema's die embedded in deze collecties staan. We hebben deze twee collecties aan elkaar gekoppeld aan de hand van lessonId in een lessonresult. Zo kan je altijd een lesresultaat vinden aan de hand een les. + + +#### Redux data diagram + +![](images/redux_state_1.png) + +Er zijn in totaal 6 reducers gemaakt. De afbeelding hierboven is de initiële staat van de store bij de startpagina. + +# Kwaliteit attributen +In dit hoofdstuk zullen kwaliteitsattributen van de Programmero applicatie beschreven worden. + +## Prestatie +De snelheid waarmee de applicatie op dit moment werkt is niet te berekenen aangezien deze lokaal wordt gedraaid. Dit is dus afhangend van de CPU die lokaal draait. In hoofdstuk 10 van dit software guidebook is een installatie gedaan op de NAS die niet representatief is voor een normale hosting die gedaan kan worden. + +De laadtijd die is vastgesteld op de lokale omgeving is onder de 1 seconde. +De laadtijd die is vastgesteld op de NAS is 15 seconden. + +## Veiligheid +De drie gebruikers die bij functioneel overzicht staan beschreven staan allemaal afgeschermd van elkaar in een eigen omgeving. We maken hier gebruik van JWT, een codebibliotheek die, wanneer je inlogt, een token bewaard in de localstorage van de client. Voor elke request die naar de server wordt gestuurd wordt deze token meegestuurd en vergeleken in de backend van de applicatie. + +## Internationalisatie +De applicatie maakt alleen gebruik van Nederlandse taal en teksten. Er is geen rekening gehouden met meerdere talen ondersteuning. + +## Compatibiliteit +De Programmero appplicatie is gemaakt voor mobiele form factor. De applicatie zal als je de applicatie op je mobiel bezoekt zich aanpassen op de resolutie van het scherm van de client. + +De Programmero appplicatie werkt stabiel op de volgende browsers: +1. Google Chrome +2. Mozilla Firefox +3. Internet Explorer 8 (en nieuwere versies) + +# Limitaties +In dit hoofdstuk worden alle limitaties waar we in dit project tegenaan lopen neergezet. + +#### Tijd +Programmero wordt gerealiseerd in 8 weken tijd waarvan er 6 weken verdeeld zijn over 3 sprints. De overige twee weken zijn verdeeld in pre-game en post-game voor voorbereiding en afwerking. + +#### Budget +Er is geen budget vastgesteld voor geen enkel onderdeel van de applicatie. Hierbij is het dus niet mogelijk om de applicatie te hosten of externe betaalde tools te gebruiken om de applicatie te realiseren. + +Dit betekent ook dat de applicatie geen gebruik kan maken van betaalde licenties van andere software, bibliotheken of frameworks. + +#### Kennis +Projectgroep aardvark bestaat uit 5 studenten die aan het begin van hun studie zitten (2e jaars). Hierbij is er een limitatie van kennis in de verschillende elementen van de applicatie en zijn technologieën. Er kan geen aansprakelijkheid worden gesteld aan de studenten voor ongedocumenteerde problemen van de applicatie. + +# Principes + +## Configuratie +Configuratie is af en toe een lastig aspect van software ontwikkeling. Om alles op te zetten is ons eerste principe dan ook de configuratie die je moet hebben om de applicatie correct te laten werken, maar ook om bijvoorbeeld testing te laten werken. + +Voor configuratie gebruiken we dotenv. Een bibliotheek die we in de `programmero-backend` en `e2e` folder gebruiken om desbetreffende artifacts op te zetten. We gaan ervan uit dat de sleutels die in de `.env.example` zelf ingevuld kunnen worden om de juiste lokale of remote configuratie de applicatie werkend te krijgen. Dit doe je door de `.env.example` te kopieëren en `.env` aan te maken en de juiste gegevens in te vullen. De bedoeling voor `programmero-backend` is hier dat je twee MongoDB databases hebt (een voor productie en een voor testing), app variabelen, een JWT secret key die je kan aamaken in `programmero-backend/scripts/createUserPassword.js` en SMPT gegevens voor de mailing. + +## Geautomatiseerd testen +Een van de andere principes die we willen hanteren is het gebruik van unit tests en end-to-end tests. Hierbij wordt er een aparte omgeving neergezet waar de functionaliteiten van het opgezette artifact worden getest. Met een end-to-end test worden geen individuele functies getest, maar wordt de applicatie doorlopen in een automatische computergestuurde browser die door de applicatie klikt om onderdelen te testen die op elkaar werken volgens een bepaalde volgorde. + +### Unit tests +Het gebruik van unit testing is om te blijven checken of functies die zijn gemaakt werken zoals ze ontworpen zijn. We testen hierbij functies en classes van verschillende onderdelen van de app in beide backend en frontend. + +Om de tests correct uit te voeren moeten er de volgende commando's uitvoerd worden voor hun respectievelijke artifacten. + +**Backend** +``` +$ cd programmero-backend +$ npm i +$ npm test -- --runInBand --silent +``` + +**Frontend** +``` +$ git checkout unittests/frontend +$ cd programmero-frontend +$ npm i +$ npm test +``` + +### End-to-end tests +End-to-end testing is het principe van een UI test die automatisch door de applicatie heen klikt en zo alles test. + +``` +$ cd e2e +$ npm i +$ npm start +``` + +# Software architectuur + +## Context diagram +![alt text](images/Context_diagram.svg "Context diagram") + +In dit bovenstaande diagram is de context van het Programmero syteem getekend. Hier zijn alle systemen waar de eindgebruiker van de applicatie interactie mee heeft om de applicatie in zijn geheel te laten werken. + +## Container diagram +![alt text](images/Container_diagram.svg "Container diagram") + +Dit container diagram is een visuele representatie van het hele Programmero systeem. Hier zijn alle interne en externe onderdelen van de applicatie weergegeven om een duidelijk beeld te krijgen hoe elk onderdeel van de applicatie met elkaar werkt op een macro grootte. + +* Web applicatie: Een met React, Redux en Javascript gemaakte front-end website waarop gebruikers acties kunnen uitvoeren. +* Web server: Een Javscript, Express en mongoDB server dat de enige plek is waar gebruikers via het Internet het systeem kunnen bereiken. +* Database: Een MongoDB/Mongoose database die Programmero gebruikt om data op te slaan. +* Mail server: Een Mailtrap.io server die gebruikt wordt om emails vanaf het systeem naar de gebruikers te sturen. + +## Component Front-end diagram +Dit diagram toont de relatie tussen alle componenten binnen het front-end systeem van Programmero. + +### __Let op:__ +* Redux Reducers komen vaker voor en staan aangegeven per kleur. Dit is gedaan omdat meerdere React componenten bepaalde Reducers gebruiken. +* Redux Actions staan niet in het diagram omdat het heel erg lastig is om deze duidelijk te tonen. Een React component kan bijvoorbeeld zonder action bij een reducer data ophalen, maar hij kan ook via een action data schrijven naar een reducer. Onder het diagram staat per React component beschreven welke action deze gebruikt. + +![alt text](images/Component_diagram_front_end.svg "Component Front-end diagram") + +Dit frontend component diagram laat de gehele werking van de frontend React componenten zien. Frontend bestaat uit 3 begrippen: components, action creators en reducers met elk hun eigen functionalteit. Hierbij zijn: +- Components: User Interface React elementen die onderdelen van de applicatie renderen en acties kunnen uitvoeren. +- Action creators: Worden door components beheerd en aangeroepen, veranderen de state van Redux door reducers aan te roepen. +- Reducers: Functies die informatie uitsluiten. + +#### Action creators per React Component: +* AddQuestion.jsx gebruikt actionCreator(s): questionManagementAction, lessonManagementAction. +* AddPassword.jsx gebruikt actionCreator(s): userActions. +* InviteUser.jsx gebruikt actionCreator(s): userActions. +* LessonManagement.jsx gebruikt actionCreator(s): lessonManagementAction. +* Login.jsx gebruikt actionCreator(s): userActions. +* QuestionEdit.jsx gebruikt actionCreator(s): questionManagementAction, lessonManagementAction. +* QuestionManagement.jsx gebruikt actionCreator(s): questionManagementAction, lessonManagementAction. +* Feedback.jsx gebruikt actionCreator(s): practiceAction, questionManagementAction. +* LessonprogramResult.jsx gebruikt actionCreator(s): userActions. +* Practice.jsx gebruikt actionCreator(s): questionManagementAction, practiceAction. +* StudentProgramms.jsx gebruikt actionCreator(s): practiceAction, questionManagementAction, lessonManagementAction. + +## Component Back-end diagram +Dit diagram toont de relatie tussen alle componenten binnen het back-end systeem van Programmero. + +### __Let op:__ +* Iedere Route gebruikt validationCheckers.js. + +![alt text](images/Component_diagram_backend.svg "Component Back-end diagram") + +Dit laatste component diagram laat de gehele werking van de backend onderdelen zien. Backend bestaat uit 3 begrippen: Routes, models en middleware met elk hun eigen functionalteit. Hierbij zijn: +- Routes: alle REST endpoints van de server. Hier staan alle URLs waar de frontend van de applicatie aanvragen naar kan versturen. +- Models: de enige functies die toegang hebben tot de database. Deze worden gebruikt in de routes om controle te hebben over de collecties die er in de database staan. +- Middleware: alle functies die voor een request worden uitgevoerd. De enige middleware die we gebruiken is een authorisatie middleware die kijkt of een gebruiker is ingelogd en zet een sessie in de request zelf. + +# Infrastructure architectuur +In dit hoofdstuk worden de specificaties van de server waarop programmero kan draaien gedocumenteerd, Programmero is succesvol getest op een NAS, aan de hand hiervan zijn de minimum systeemeisen opgesteld. + +#### Minimale specificaties + +| Specificatie | Waarde | +| -------------------- | ------------------------ | +| Modelnaam | DS216+ | +| CPU | INTEL Celeron N3050 | +| Kloksnelheid van CPU | 1.6 GHz | +| CPU-kernen | 2 | +| Fysiek geheugen | 1024 MB | +| Operating system | DSM 6.2.1-23824 Update 4 | + +De setup is getest door node, git en npm te installeren. MongoDB draait als een container in een virtuele omgeving, vooral het opstarten van deze software is zwaar. Om deze reden is er geen deamon aangemaakt op de nas. + +Op een server met gelijkwaarde specificaties kan Programmero draaien, dit raad het Aardvark team af. In deze omgeving wordt de deployment omgeving gezet. Omdat dit een hobby project is worden er geen credentials en procedures vrij gegeven, dit heeft te maken met non disclosure en veiligheid. Zie hoofdstuk aangeraden specificaties en installatie handleiding voor uitgebreidere installatieinstructies. + +#### Aangeraden specificaties + +| Specificatie | Waarde | +| -------------------- | ------------------------------------------------------------ | +| CPU | Processorarchitectuur AMD64 (een 64 bit AMD of Intel processor) | +| Kloksnelheid van CPU | 3 Ghz of meer | +| CPU-kernen | 4 | +| Fysiek geheugen | 4GB of meer | +| Operating system | Ubuntu 18.04.1 LTS Bionic (Meest recente LTS versie is ook goed) | + +# Onderhoud en ondersteuning +Dit hoofdstuk beschrijft alle acties die na de voltooiing van het project uitgevoerd worden en welke niet. + +## Onderhoud +Onderhoud van dit product valt niet onder de verantwoordelijkheid van de eerste projectgroep (Aardvark). Maar valt onder de opdracht van de productowner waar het product aan overhandigd is. De productowner kan aan de hand van gemaakte documentatie en applicatie het project hosten en/of overhandigen aan een andere projectgroep. + +Projectgroep Aardvark is wel verantwoordelijk voor documentatie rond overhandiging van de applicatie. Dit betekent dat de projectgroep een zo'n goed mogelijke visuele en tekstuele representatie van de applicatie in beeld kunnen schetsen in het installatie handboek, dit software guide book en het PvA. Dit is zodat toekomstige projectgroepen de applicatie beter kunnen analyseren, sneller kunnen testen en wijzigingen kunnen toevoegen. + +## Ondersteuning +Dit product krijgt van de eerste projectgroep (Aardvark) geen verdere ondersteuning na de projectperiode van 8 weken. Deze ondersteuning houdt in: er zullen geen extra functionaliteiten, bug fixes, issues, userstories of projectborden worden aangemaakt, aangepast of bijgehouden. \ No newline at end of file diff --git a/documentation/Software Guidebook/c4_model_levels.gliffy b/documentation/Software Guidebook/c4_model_levels.gliffy new file mode 100644 index 0000000..2f927d0 --- /dev/null +++ b/documentation/Software Guidebook/c4_model_levels.gliffy @@ -0,0 +1 @@ +{"contentType":"application/gliffy+json","version":"1.3","stage":{"background":"#FFFFFF","width":3552,"height":1140,"nodeIndex":208,"autoFit":true,"exportBorder":false,"gridOn":false,"snapToGrid":true,"drawingGuidesOn":true,"pageBreaksOn":false,"printGridOn":false,"printPaper":null,"printShrinkToFit":false,"printPortrait":false,"maxWidth":5000,"maxHeight":5000,"themeData":null,"imageCache":{},"viewportType":"default","fitBB":{"min":{"x":52,"y":184},"max":{"x":3552,"y":1140}},"printModel":{"pageSize":"Letter","portrait":true,"fitToOnePage":false,"displayPageBreaks":false},"objects":[{"x":3372.0,"y":364.0,"rotation":0.0,"id":206,"width":117.33333333333348,"height":126.66666666666669,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":187,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":187,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":199,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-7.0,-2.0],[118.0,126.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":3366.6666666666665,"y":364.0,"rotation":0.0,"id":204,"width":62.666666666666515,"height":120.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":185,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":187,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":189,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-1.666666666666515,-2.0],[-66.66666666666652,126.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2702.6666666666665,"y":361.3333333333333,"rotation":0.0,"id":185,"width":125.33333333333348,"height":284.00000000000006,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":159,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":149,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":180,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.666666666666515,0.6666666666666856],[127.33333333333348,287.6666666666667]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1952.0,"y":561.3333333333334,"rotation":0.0,"id":178,"width":114.66666666666652,"height":32.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":147,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":116,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":138,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[8.0,-163.33333333333337],[118.0,38.66666666666663]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1945.3333333333333,"y":562.6666666666666,"rotation":0.0,"id":176,"width":106.66666666666652,"height":38.66666666666674,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":146,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":116,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":121,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[14.666666666666742,-164.66666666666663],[-105.33333333333326,37.33333333333337]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2702.6666666666665,"y":540.0,"rotation":0.0,"id":174,"width":74.66666666666652,"height":105.33333333333337,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":145,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":149,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":165,"py":0.0,"px":0.5666666666666667}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.666666666666515,-178.0],[-74.66666666666652,109.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2634.6666666666665,"y":637.3333333333334,"rotation":0.0,"id":170,"width":194.66666666666652,"height":16.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":144,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":165,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":136,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-74.66666666666652,57.66666666666663],[-204.66666666666652,20.16666666666663]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1886.6666666666667,"y":670.6666666666666,"rotation":0.0,"id":162,"width":93.33333333333326,"height":82.66666666666674,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":132,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":121,"py":1.0,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":126,"py":0.0,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[13.333333333333258,7.333333333333371],[93.33333333333326,64.33333333333337]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1982.6666666666667,"y":636.0,"rotation":0.0,"id":159,"width":60.0,"height":1.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":129,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":138,"py":0.5,"px":0.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":121,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[27.333333333333258,3.0],[-82.66666666666674,3.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":161,"width":78.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

login mogelijk

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1865.3333333333333,"y":673.3333333333334,"rotation":0.0,"id":156,"width":6.666666666666515,"height":80.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":126,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":121,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":131,"py":0.0,"px":0.5}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-25.333333333333258,4.666666666666629],[-25.333333333333258,61.66666666666663]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":157,"width":102.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Logt in als student

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2369.0,"y":713.0,"rotation":0.0,"id":146,"width":265.0,"height":70.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":112,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":136,"py":0.2928932188134525,"px":1.1102230246251563E-16}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":138,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-39.0,-71.03300858899104],[-239.0,-74.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":155,"width":152.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":0.520884949826026,"linePerpValue":0.0,"cardinalityType":null,"html":"

Stuurt mail met code om te 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2132.0,"y":781.6666666666666,"rotation":0.0,"id":143,"width":272.0,"height":16.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":109,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":126,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":165,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-2.0,-1.6666666666666288],[428.0,-86.66666666666663]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":154,"width":226.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":0.5257501788849891,"linePerpValue":0.0,"cardinalityType":null,"html":"

Verstuurt een uitnodiging naar student via

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":50.0,"y":1112.0,"rotation":0.0,"id":111,"width":150.0,"height":28.0,"uid":"com.gliffy.shape.basic.basic_v1.default.text","order":50,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Software system

Programmero

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":70.0,"y":530.0,"rotation":0.0,"id":108,"width":840.0,"height":610.0,"uid":"com.gliffy.shape.basic.basic_v1.default.rectangle","order":0,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":855.0,"y":729.0,"rotation":0.0,"id":104,"width":115.0,"height":40.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":47,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":73,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":83,"py":0.7071067811865475,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[5.0,-3.0],[275.0,-45.96699141100896]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":107,"width":190.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Verzend email met het gebruik van

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":580.0,"y":842.0,"rotation":0.0,"id":95,"width":162.0,"height":166.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":44,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":73,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":0,"py":0.5,"px":1.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[120.0,-7.0],[-150.0,158.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":98,"width":100.0,"height":28.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Leest data van en

schrijft data naar

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":434.0,"y":731.0,"rotation":0.0,"id":89,"width":103.0,"height":1.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":41,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":59,"py":0.5,"px":1.0}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":73,"py":0.5,"px":0.0}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":2,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-4.0,-5.0],[106.0,-5.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":92,"width":50.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Gebruikt

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1130.0,"y":630.0,"rotation":0.0,"id":83,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":38,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":85,"width":96.0,"height":42.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Mail-server

[mailtrap.io]

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":386.0,"y":492.0,"rotation":0.0,"id":68,"width":18.0,"height":143.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":24,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":7,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":59,"py":0.0,"px":0.836}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[60.5,-65.0],[3.0,125.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":72,"width":50.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.37816940913518843,"linePerpValue":0.0,"cardinalityType":null,"html":"

Gebruikt

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":315.0,"y":493.0,"rotation":0.0,"id":67,"width":43.0,"height":148.0,"uid":"com.gliffy.shape.erd.erd_v1.default.one","order":21,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[],"startConstraint":{"type":"StartPositionConstraint","StartPositionConstraint":{"nodeId":4,"py":1.0,"px":0.5}},"endConstraint":{"type":"EndPositionConstraint","EndPositionConstraint":{"nodeId":59,"py":0.0,"px":0.28}}},"graphic":{"type":"Line","Line":{"strokeWidth":2.0,"strokeColor":"#000000","fillColor":"none","dashStyle":null,"startArrow":0,"endArrow":0,"startArrowRotation":"auto","endArrowRotation":"auto","interpolationType":"linear","cornerRadius":null,"controlPath":[[-41.5,-66.0],[-65.0,124.0]],"lockSegments":{},"ortho":false}},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":71,"width":50.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":0.46153573056588704,"linePerpValue":0.0,"cardinalityType":null,"html":"

Gebruikt

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":415.0,"y":345.0,"rotation":0.0,"id":7,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.user","order":7,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":9,"width":48.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Leerling

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":242.0,"y":345.0,"rotation":0.0,"id":4,"width":63.0,"height":82.0,"uid":"com.gliffy.shape.network.network_v4.business.user","order":4,"lockAspectRatio":true,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.network.network_v4.business.user","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#3966A0","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":8,"width":43.0,"height":14.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"both","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Docent

","tid":null,"valign":"middle","vposition":"below","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":180.0,"y":920.0,"rotation":0.0,"id":0,"width":250.0,"height":160.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.database","order":1,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.database.flowchart_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":4.999999999999998,"y":0.0,"rotation":0.0,"id":93,"width":239.99999999999994,"height":196.0,"uid":null,"order":"auto","lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":15,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":15,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Database (MongoDB)

[Container :  Database schema ERD]

 

 

 

Slaat de opgegeven persoonlijke informatie van leerlingen en docenten,  de aangemaakte lesprogramma's/lessen en vragen van de docent en de resultaten van een student op 

 

 

 

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":180.0,"y":617.0,"rotation":0.0,"id":59,"width":250.0,"height":218.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":10,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":60},{"magnitude":1,"id":63}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":60,"width":250.0,"height":46.0,"uid":null,"order":12,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":61}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":61,"width":250.0,"height":46.0,"uid":null,"order":15,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Web applicatie

[Container : Javascript en React] 

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":46.0,"rotation":0.0,"id":62,"width":250.0,"height":172.0,"uid":null,"order":17,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":59},{"magnitude":-1,"id":60}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":60,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":63,"width":250.0,"height":158.0,"uid":null,"order":20,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

Voor Docent: Maakt het mogelijk om lesprogramma''s, lessen en vragen te beheren.  Ook kan er een student toegevoegd worden waarop een invite mail word gestuurd.

 

Voor Student: De student kan alle lesprogramma's zien en daarin oefenen met code kaarten waarop hij zijn resultaten met score kan overzien.

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":540.0,"y":617.0,"rotation":0.0,"id":73,"width":320.0,"height":218.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":27,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":74},{"magnitude":1,"id":77}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":74,"width":320.0,"height":46.0,"uid":null,"order":29,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":75}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":75,"width":320.0,"height":46.0,"uid":null,"order":32,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Web Server

[Container : Javascript, Express en mongoDB] 

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":46.0,"rotation":0.0,"id":76,"width":320.0,"height":172.0,"uid":null,"order":34,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":73},{"magnitude":-1,"id":74}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":74,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":77,"width":320.0,"height":60.0,"uid":null,"order":37,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

Behandeld HTTP requests door de web applicatie met mongoDB.

Checkt op authorisatie en authenticatie.

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1810.0,"y":200.0,"rotation":0.0,"id":116,"width":300.0,"height":198.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":51,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":117},{"magnitude":1,"id":120}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":117,"width":300.0,"height":54.0,"uid":null,"order":53,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":118}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":118,"width":300.0,"height":54.0,"uid":null,"order":56,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Web applicatie

[Container : Javascript en React] 

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":54.0,"rotation":0.0,"id":119,"width":300.0,"height":144.0,"uid":null,"order":58,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":116},{"magnitude":-1,"id":117}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":117,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":120,"width":300.0,"height":144.0,"uid":null,"order":61,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

Voor Docent: Maakt het mogelijk om lesprogramma''s, lessen en vragen te beheren.  Ook kan er een student toegevoegd worden waarop een invite mail word gestuurd.

 

Voor Student: De student kan alle lesprogramma's zien en daarin oefenen met code kaarten waarop hij zijn resultaten met score kan overzien.

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1780.0,"y":600.0,"rotation":0.0,"id":121,"width":120.0,"height":78.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":62,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":122},{"magnitude":1,"id":125}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":122,"width":120.0,"height":18.0,"uid":null,"order":64,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":123}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":123,"width":120.0,"height":18.0,"uid":null,"order":67,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Log in 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":124,"width":120.0,"height":60.0,"uid":null,"order":69,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":121},{"magnitude":-1,"id":122}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":122,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":125,"width":120.0,"height":60.0,"uid":null,"order":72,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Zorgt voor onderscheiding tussen twee gebruikers.

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1980.0,"y":735.0,"rotation":0.0,"id":126,"width":150.0,"height":90.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":73,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":127},{"magnitude":1,"id":130}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":127,"width":150.0,"height":18.0,"uid":null,"order":75,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":128}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":128,"width":150.0,"height":18.0,"uid":null,"order":78,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Beheer

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":129,"width":150.0,"height":72.0,"uid":null,"order":80,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":126},{"magnitude":-1,"id":127}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":127,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":130,"width":150.0,"height":60.0,"uid":null,"order":83,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Geeft de mogelijkeheid om lesprogramma's,lessen en vragen te beheren en studenten uit te nodigen

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":1770.0,"y":735.0,"rotation":0.0,"id":131,"width":140.0,"height":90.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":84,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":132},{"magnitude":1,"id":135}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":132,"width":140.0,"height":18.0,"uid":null,"order":86,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":133}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":133,"width":140.0,"height":18.0,"uid":null,"order":89,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Oefenen

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":134,"width":140.0,"height":72.0,"uid":null,"order":91,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":131},{"magnitude":-1,"id":132}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":132,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":135,"width":140.0,"height":32.0,"uid":null,"order":94,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 Geeft de mogelijkeheid om op vragen te oefenen

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2330.0,"y":620.0,"rotation":0.0,"id":136,"width":100.0,"height":75.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.process","order":95,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":2.0,"y":0.0,"rotation":0.0,"id":137,"width":96.0,"height":42.0,"uid":null,"order":97,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Mail-server

[mailtrap.io]

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2010.0,"y":600.0,"rotation":0.0,"id":138,"width":120.0,"height":78.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":98,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":139},{"magnitude":1,"id":142}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":139,"width":120.0,"height":18.0,"uid":null,"order":100,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":140}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":140,"width":120.0,"height":18.0,"uid":null,"order":103,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Registreren

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":141,"width":120.0,"height":60.0,"uid":null,"order":105,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":138},{"magnitude":-1,"id":139}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":139,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":142,"width":120.0,"height":60.0,"uid":null,"order":108,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Zorgt voor onderscheiding tussen twee gebruikers.

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2540.0,"y":200.0,"rotation":0.0,"id":149,"width":320.0,"height":162.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":115,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":150},{"magnitude":1,"id":153}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":150,"width":320.0,"height":52.0,"uid":null,"order":117,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":151}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":151,"width":320.0,"height":52.0,"uid":null,"order":120,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Web Server

[Container : Javascript, Express en mongoDB] 

 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":52.0,"rotation":0.0,"id":152,"width":320.0,"height":110.0,"uid":null,"order":122,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":149},{"magnitude":-1,"id":150}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":150,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":153,"width":320.0,"height":60.0,"uid":null,"order":125,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":6,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 

Behandeld HTTP requests door de web applicatie met mongoDB.

Checkt op authorisatie en authenticatie.

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2560.0,"y":649.0,"rotation":0.0,"id":165,"width":120.0,"height":92.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":133,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":166},{"magnitude":1,"id":169}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":166,"width":120.0,"height":18.0,"uid":null,"order":135,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":167}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":167,"width":120.0,"height":18.0,"uid":null,"order":138,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

 MailHandler

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":168,"width":120.0,"height":74.0,"uid":null,"order":140,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":165},{"magnitude":-1,"id":166}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":166,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":169,"width":120.0,"height":74.0,"uid":null,"order":143,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Handeld request van versturen van mail af en zegt in welk format mail verstuurd moet worden 

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":2770.0,"y":649.0,"rotation":0.0,"id":180,"width":120.0,"height":91.99999999999999,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":148,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":181},{"magnitude":1,"id":184}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":181,"width":120.0,"height":18.0,"uid":null,"order":150,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":182}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":182,"width":120.0,"height":18.0,"uid":null,"order":153,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

RoutesHanler

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":183,"width":120.0,"height":73.99999999999999,"uid":null,"order":155,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":180},{"magnitude":-1,"id":181}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":181,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":184,"width":120.0,"height":46.0,"uid":null,"order":158,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Handeld alle requests af van de web applicatie

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":3240.0,"y":202.0,"rotation":0.0,"id":187,"width":250.0,"height":160.0,"uid":"com.gliffy.shape.flowchart.flowchart_v1.default.database","order":160,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.database.flowchart_v1","strokeWidth":2.0,"strokeColor":"#333333","fillColor":"#ffffff","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":0.0,"shadowY":0.0}},"linkMap":[],"children":[{"x":4.999999999999998,"y":0.0,"rotation":0.0,"id":188,"width":239.99999999999994,"height":196.0,"uid":null,"order":162,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":8,"paddingRight":8,"paddingBottom":8,"paddingLeft":8,"outerPaddingTop":15,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":15,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Database (MongoDB)

[Container :  Database schema ERD]

 

 

 

Slaat de opgegeven persoonlijke informatie van leerlingen en docenten,  de aangemaakte lesprogramma's/lessen en vragen van de docent en de resultaten van een student op 

 

 

 

 

","tid":null,"valign":"middle","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":3240.0,"y":490.0,"rotation":0.0,"id":189,"width":120.0,"height":78.0,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":163,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":190},{"magnitude":1,"id":193}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":190,"width":120.0,"height":32.0,"uid":null,"order":165,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":191}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":191,"width":120.0,"height":32.0,"uid":null,"order":168,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Database schema (ERD)

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":32.0,"rotation":0.0,"id":192,"width":120.0,"height":46.0,"uid":null,"order":170,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":189},{"magnitude":-1,"id":190}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":190,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":193,"width":120.0,"height":46.0,"uid":null,"order":173,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

Overzicht van database structuur in een ERD formaat

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":3430.0,"y":490.0,"rotation":0.0,"id":199,"width":120.0,"height":60.00000000000001,"uid":"com.gliffy.shape.erd.erd_v1.default.entity_with_attributes","order":174,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":true,"heightInfo":[{"magnitude":1,"id":200},{"magnitude":1,"id":203}],"minHeight":0.0,"growParent":false,"padding":0.0}}]},"linkMap":[],"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":200,"width":120.0,"height":18.0,"uid":null,"order":176,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":201}],"minHeight":0.0,"growParent":true,"padding":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":201,"width":120.0,"height":18.0,"uid":null,"order":179,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

NoSQL database

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"},{"x":0.0,"y":18.0,"rotation":0.0,"id":202,"width":120.0,"height":42.00000000000001,"uid":null,"order":181,"lockAspectRatio":false,"lockShape":false,"constraints":{"constraints":[{"type":"HeightConstraint","HeightConstraint":{"isMin":false,"heightInfo":[{"magnitude":1,"id":199},{"magnitude":-1,"id":200}],"minHeight":20.0,"growParent":false,"padding":0.0}},{"type":"PositionConstraint","PositionConstraint":{"nodeId":200,"px":0.0,"py":1.0,"xOffset":0.0,"yOffset":0.0}}]},"graphic":{"type":"Shape","Shape":{"tid":"com.gliffy.stencil.rectangle.basic_v1","strokeWidth":1.0,"strokeColor":"#000000","fillColor":"#FFFFFF","gradient":false,"dashStyle":null,"dropShadow":false,"state":0,"opacity":1.0,"shadowX":4.0,"shadowY":4.0}},"children":[{"x":0.0,"y":0.0,"rotation":0.0,"id":203,"width":120.0,"height":18.0,"uid":null,"order":184,"lockAspectRatio":false,"lockShape":false,"graphic":{"type":"Text","Text":{"overflow":"none","paddingTop":2,"paddingRight":2,"paddingBottom":2,"paddingLeft":2,"outerPaddingTop":6,"outerPaddingRight":6,"outerPaddingBottom":2,"outerPaddingLeft":5,"type":"fixed","lineTValue":null,"linePerpValue":null,"cardinalityType":null,"html":"

MongoDB

","tid":null,"valign":"top","vposition":"none","hposition":"none"}},"children":[],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"hidden":false,"layerId":"vcBVOa32RKVg"}],"layers":[{"guid":"vcBVOa32RKVg","order":0,"name":"Layer 0","active":true,"locked":false,"visible":true,"nodeIndex":189}],"shapeStyles":{},"lineStyles":{"global":{"endArrow":2}},"textStyles":{"global":{"size":"14px"}}},"metadata":{"title":"untitled","revision":0,"exportBorder":false,"loadPosition":"default","autosaveDisabled":false,"lastSerialized":1544707756652,"libraries":["com.gliffy.libraries.network.network_v4.business","com.gliffy.libraries.basic.basic_v1.default","com.gliffy.libraries.flowchart.flowchart_v1.default","com.gliffy.libraries.swimlanes.swimlanes_v1.default","com.gliffy.libraries.uml.uml_v2.class","com.gliffy.libraries.uml.uml_v2.sequence","com.gliffy.libraries.uml.uml_v2.activity","com.gliffy.libraries.erd.erd_v1.default","com.gliffy.libraries.ui.ui_v3.containers_content","com.gliffy.libraries.ui.ui_v3.forms_controls","com.gliffy.libraries.images"]},"embeddedResources":{"index":0,"resources":[]}} \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/10_Infrastructure_architectuur.md b/documentation/Software Guidebook/chapters/10_Infrastructure_architectuur.md new file mode 100644 index 0000000..8d24250 --- /dev/null +++ b/documentation/Software Guidebook/chapters/10_Infrastructure_architectuur.md @@ -0,0 +1,27 @@ +# Infrastructure architectuur +In dit hoofdstuk worden de specificaties van de server waarop programmero kan draaien gedocumenteerd, Programmero is succesvol getest op een NAS, aan de hand hiervan zijn de minimum systeemeisen opgesteld. + +#### Minimale specificaties + +| Specificatie | Waarde | +| -------------------- | ------------------------ | +| Modelnaam | DS216+ | +| CPU | INTEL Celeron N3050 | +| Kloksnelheid van CPU | 1.6 GHz | +| CPU-kernen | 2 | +| Fysiek geheugen | 1024 MB | +| Operating system | DSM 6.2.1-23824 Update 4 | + +De setup is getest door node, git en npm te installeren. MongoDB draait als een container in een virtuele omgeving, vooral het opstarten van deze software is zwaar. Om deze reden is er geen deamon aangemaakt op de nas. + +Op een server met gelijkwaarde specificaties kan Programmero draaien, dit raad het Aardvark team af. In deze omgeving wordt de deployment omgeving gezet. Omdat dit een hobby project is worden er geen credentials en procedures vrij gegeven, dit heeft te maken met non disclosure en veiligheid. Zie hoofdstuk aangeraden specificaties en installatie handleiding voor uitgebreidere installatieinstructies. + +#### Aangeraden specificaties + +| Specificatie | Waarde | +| -------------------- | ------------------------------------------------------------ | +| CPU | Processorarchitectuur AMD64 (een 64 bit AMD of Intel processor) | +| Kloksnelheid van CPU | 3 Ghz of meer | +| CPU-kernen | 4 | +| Fysiek geheugen | 4GB of meer | +| Operating system | Ubuntu 18.04.1 LTS Bionic (Meest recente LTS versie is ook goed) | \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/11_Onderhoud_en_ondersteuning.md b/documentation/Software Guidebook/chapters/11_Onderhoud_en_ondersteuning.md new file mode 100644 index 0000000..10c9f6f --- /dev/null +++ b/documentation/Software Guidebook/chapters/11_Onderhoud_en_ondersteuning.md @@ -0,0 +1,10 @@ +# Onderhoud en ondersteuning +Dit hoofdstuk beschrijft alle acties die na de voltooiing van het project uitgevoerd worden en welke niet. + +## Onderhoud +Onderhoud van dit product valt niet onder de verantwoordelijkheid van de eerste projectgroep (Aardvark). Maar valt onder de opdracht van de productowner waar het product aan overhandigd is. De productowner kan aan de hand van gemaakte documentatie en applicatie het project hosten en/of overhandigen aan een andere projectgroep. + +Projectgroep Aardvark is wel verantwoordelijk voor documentatie rond overhandiging van de applicatie. Dit betekent dat de projectgroep een zo'n goed mogelijke visuele en tekstuele representatie van de applicatie in beeld kunnen schetsen in het installatie handboek, dit software guide book en het PvA. Dit is zodat toekomstige projectgroepen de applicatie beter kunnen analyseren, sneller kunnen testen en wijzigingen kunnen toevoegen. + +## Ondersteuning +Dit product krijgt van de eerste projectgroep (Aardvark) geen verdere ondersteuning na de projectperiode van 8 weken. Deze ondersteuning houdt in: er zullen geen extra functionaliteiten, bug fixes, issues, userstories of projectborden worden aangemaakt, aangepast of bijgehouden. \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/1_Introductie.md b/documentation/Software Guidebook/chapters/1_Introductie.md new file mode 100644 index 0000000..2e5db27 --- /dev/null +++ b/documentation/Software Guidebook/chapters/1_Introductie.md @@ -0,0 +1,10 @@ +# Introductie +Dit software guidebook zal inzicht geven op vele aspecten van de volledige Programmero applicatie. De hoofdstukken zijn opgedeeld over verschillende bestanden in de map `documentation/Software Guidebook/chapters` aan de hand van een [website](https://leanpub.com/techtribesje/read#introduction) en de inhoudsopgave van het boek dat op de website wordt aangegeven. Daarbij heeft de projectgroep het hoofdstuk "REST endpoints" toegevoegd omdat we deze documentatie belangrijk vinden en het een toevoeging heeft voor het software guidebook en de gebruikers en administrators van Programmero. Het hoofdstuk "Implementatie" is weggehaald in het SGB omdat deze is gedocumenteerd in de `Readme.md` van dit project. + +Om een duidelijke focus te leggen op wat het software guidebook te documenteren heeft zullen we dit samenvatten in een paar punten: +- De context, probleem en oplossing van de applicatie +- De requirements, limitaties en principes van de applicatie +- Een overzicht van de mogelijkheden die de backend van de applicatie bied in de format van REST endpoints +- De software architectuur van de applicatie op beide hoog niveau en in detail +- Infrastructuur van de applicatie +- Onderhoud en ondersteuning na implementatie van de applicatie \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/2_Context.md b/documentation/Software Guidebook/chapters/2_Context.md new file mode 100644 index 0000000..1b79d7e --- /dev/null +++ b/documentation/Software Guidebook/chapters/2_Context.md @@ -0,0 +1,23 @@ +# Context +In dit hoofdstuk zal de context van de applicatie Programmero worden gedocumenteerd. Dit hoofdstuk is erg belangrijk om de intentie en motivatie van het probleem in te zien. In het volgende hoofdstuk zal worden ingegaan hoe dit probleem in zijn geheel wordt opgelost met de applicatie. + +## Probleemstelling +Programmero is een applicatie die docenten van de HAN (hogeschool van Arnhem -en Nijmegen) een nieuwe manier van het vak SPD geven zou moeten aanbieden. Docenten hebben hierbij aangegeven dat bepaalde studenten die het vak SPD volgen, soms niet goed kunnen meegaan in de lessen. De docenten van de HAN hebben onderzoek gedaan naar lesmethoden die anders zijn om zo de studenten te helpen die moeite hebben om het huidige lesprogramma te volgen. + +De huidige lesmethode valt onder het "whole-word" principe. Hierbij worden hele woorden gebruikt om programmeertalen uit te leggen en te oefenen. Hierbij wilt de productowner een applicatie hebben die een "phonix" principe aanpakt. Het "phonix" principe bied een lesstijl aan die een taal en grammatica opdelen in kleine stukken ophakt en daarin vragen kan neerzetten om mee te oefenen. Hiermee kan de productowner deze lesmethode aan zijn studenten aanbieden als ze moeite hebben met het "whole-word" principe in de lessen? + +## Uitwerking +Het doel van de applicatie is: +- Een unieke applicatie neer te zetten voor studenten om nieuwe leermethodiek te promoten +- Vragen en lesprogramma's aanbied voor studenten die in deze leeromgeving moeten oefenen met een programmeertaal +- Docenten de macht geven om lesprogramma's en vragen te beheren + +Het product zal bestaan uit twee onderdelen: de backend die alle aanvragen behandeld en die toegang heeft tot de database. En de React frontend die kan worden bezocht door gebruikers en door studenten en docenten kan worden gebruikt. Deze twee onderdelen vormen samen de applicatie. + +## Gebruikers +Programmero heeft drie typen gebruikers: +- Anonieme gebruiker +- Student +- Docent + +Elk van deze gebruikers zal nog worden toegelicht onder hoofdstuk 3: functionele overview \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/3_Functioneel_overzicht.md b/documentation/Software Guidebook/chapters/3_Functioneel_overzicht.md new file mode 100644 index 0000000..298e312 --- /dev/null +++ b/documentation/Software Guidebook/chapters/3_Functioneel_overzicht.md @@ -0,0 +1,30 @@ +# Functioneel overzicht +Dit hoofdstuk zal ingaan op alle inhoud die in de applicatie van Programmero zit. Dit gaat over informatie die de applicatie geeft, maar ook functionaliteit die de applicatie aanbied om zijn doel te bereiken. + +## Lesprogramma's en codekaarten +Programmero bestaat uit: +- Lesprogramma - een lesprogramma is een lijst van codekaarten die: + - De programmeertaal en structuur van een lesprogramma definiëren + - Een oefening neerzet voor studenten +- Codekaart - een entiteit die uit de volgende onderdelen bestaat: + - Een vraag gekoppeld aan de programmeertaal van het lesprogramma + - Een antwoord die in phonix stijl is geformuleerd +- Score - Een visuele representatie van de score van een student of lesprogramma resultaten +- Student - Gebruiker die oefeningen kan doen op lesprogramma's die door een docent zijn gemaakt en daardoor punten kan scoren +- Docent - Gebruiker die lesprogramma's en codekaarten binnen een lesprogramma beheert + +Binnen Programmero zijn lesprogramma's en codekaarten de kern van de applicatie. Een lesprogramma is een lijst van codekaarten die een docent kan aanmaken. Een student kan aan de hand van een lesprogramma vervolgens codekaarten invullen om het lesprogramma te voltooien en zo een oefening te voldoen. Daarbij kan de student bij elke oefening een score behalen. + +## Gebruikers +#### Anonieme gebruikers +Anonieme gebruikers representeren alle gebruikers die geen account of inloggegevens hebben. Anonieme gebruikers kunnen niet in de applicatie komen en kunnen alleen het inlogscherm zien. + +#### Student gebruiker +Student gebruikers representeren alle gebruikers die een account hebben met studentfunctionaliteit. Dit houdt in dat ze oefeningen kunnen maken van de lesprogramma's die in de applicatie staan en hier een score mee kunnen behalen. +Een student kan aan de hand van informatie die een leraar heeft een email ontvangen om een account te activeren. + +#### Docent gebruiker +Docent gebruikers representeren gebruikers die een administrator account hebben. Hierbij kunnen docenten lesprogramma's en codekaarten aanmaken om ervoor te zorgen dat studenten kunnen oefenen aan deze lesprogrammas's. Ook kan een leraar een gebruiker toevoegen aan het systeem door een email en naam in te voeren. Vervolgens kan deze gebruiker een eigen wachtwoord invoeren. + +## Gamificatie +In de applicatie is een vorm van gamificatie ingebouwd. Dit is een een basisvorm geïmplementeerd door een score per student, lesprogramma en vraag te berekenen. Deze scores kunnen vergeleken worden met studenten zodat er een competitieve omgeving kan worden gecreëerd. In de toekomst kan er ook op deze basis een competitieve vorm worden gemaakt waarin een student een andere student kan uitdagen in een in-app competitieve omgeving. Deze functionaliteit is uitgeschreven in [userstory 4](https://github.com/HANICA-DWA/sep2018-project-aardvark/issues/5). \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/4_REST_endpoints.md b/documentation/Software Guidebook/chapters/4_REST_endpoints.md new file mode 100644 index 0000000..80443b8 --- /dev/null +++ b/documentation/Software Guidebook/chapters/4_REST_endpoints.md @@ -0,0 +1,53 @@ +# REST API reference + +Dit hoofdstuk zal alle REST endpoints documenteren. Dit is zodat er een duidelijk overzicht is voor developers om te zien waar ze informatie kunnen halen, geven, aanpassen en verwijderen. De REST backend server gebruikt Node.JS en heeft MongoDB en ExpressJS geïnstalleerd staan. + +Alle inkomende aanvragen (requests) moeten met JSON worden aangevraagd. Hierbij moet er gebruik gemaakt worden van MIME-type "application/json" in de HTTP request header. + +Het returntype van elk endpoint in de server is JSON. Hierbij zal het MIME-type "application/json" in de HTTP header worden meegegeven. + +## Authenticatie + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /auth/:mailToken/check | - | Checkt of een mailtoken van een gebruiker bestaat | HTTP 200: String
HTTP 400: Object | +| POST | /auth/login | `email: String`
`password: String` | Logt een gebruiker in op basis van gegeven email en password | HTTP 200: Webtoken
HTTP 422: Object
HTTP 500: Object | +| POST | /auth/secret | `mailToken: String`
`pass: String`
`passrepeat: String` | Valideert wachtwoord en zet een wachtwoord voor de gebruiker met aangegeven mailtoken | HTTP 200: No content
HTTP 400: Object | + +## Lesprogramma's + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /lessons | - | Geeft alle lesprogramma's terug | HTTP 200: Array
HTTP 401: Object
HTTP 500: Object | +| GET | /lessons/:lessonId | - | Geeft een specifiek lesprogramma terug op basis van het meegegeven ID | HTTP 200: Object
HTTP 401: Object
HTTP 422: Object
HTTP 404: Object
HTTP 500: Object | +| POST | /lessons | `name: String`
`description: String`
`programmingLanguage: String` | Maakt een nieuw lesprogramma aan de hand van meegegeven POST gegevens | HTTP 201: Object
HTTP 401: Object
HTTP 422: Object
HTTP 500: Object | +| PUT | /lessons/:lessonId | `name: String`
`description: String`
`programmingLanguage: String` | Past een bestaand lesprogramma aan aan de hand van meegegeven PUT gegevens | HTTP 200: Object
HTTP 401: Object
HTTP 422: Object
HTTP 500: Object | +| DELETE | /lessons/:lessonId | - | Verwijderd een specifiek lesprogramma op basis van het meegegeven ID | HTTP 204: No Content
HTTP 401: Object
HTTP 422: Object
HTTP 500: Object | + +## Codekaarten + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /lessons/:lessonId/codecards | - | Geeft alle codekaarten van een specifiek lesprogramma op basis van meegegeven ID | HTTP 200: Array
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| GET | /lessons/:lessonId/codecards/:codecardId | - | Geeft een specifieke codekaart in een lesprogramma terug op basis van meegegeven IDs | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| GET | /lessons/:lessonId/codecards/student | - | Geeft een codekaart in een lesprogramma terug op basis van meegegeven ID en lessonresult dat de student heeft Zorgt ervoor dat het antwoord door elkaar gehusseld is | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 409: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /lessons/:lessonId/codecards | `question: String`
`answer: [String]` | Maakt een nieuwe codekaart in een lesprogramma aan de hand van meegegeven POST gegevens en ID | HTTP 201: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| PUT | /lessons/:lessonId/codecards/:codecardId | `question: String`
`answer: [String]` | Past een bestaande codekaart in een lesprogramma aan aan de hand van meegegeven PUT gegevens en IDs | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| DELETE | /lessons/:lessonId/codecards/:codecardId | - | Verwijderd een specifieke codekaart in een lesprogramma op basis van het meegegeven ID | HTTP 204: No content
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | + +## Oefenresultaten + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /users/lessonresult/:lessonId/end | - | Haalt oefenresultaten op van laatste oefening die in een les is gemaakt op basis van meegegeven ID | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /users/lessonresult/:lessonId | - | Maakt een nieuwe lessonresult, zodat een student met een oefening kan beginnen | HTTP 201: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /users/lessonresult/:lessonId/answer | `answer: [String]`
`index: Int` | Voegt een nieuw antwoord toe aan een lessonresult | HTTP 200: Object
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | + + +## Gebruikers + +| Methode | Pad | Body/Payload | Omschrijving | Response | +|---------|-----|------|--------------|----------| +| GET | /users/score | - | Vraagt de totaalscore van een gebruiker op | HTTP 200: Number
HTTP 401: Object
HTTP 500: Object | +| GET | /users/score/:lessonid | - | Vraagt de score van een specifieke les van een gebruiker op | HTTP 200: Number
HTTP 401: Object
HTTP 404: Object
HTTP 422: Object
HTTP 500: Object | +| POST | /users | `name: String`
`email: String`
`isAdmin: Boolean` | Maakt een nieuwe user aan en stuurt een mail naar naar de desbetreffende user | HTTP 201: No content
HTTP 400: Object
HTTP 422: Object
HTTP 500: Object | diff --git a/documentation/Software Guidebook/chapters/5_data_structure.md b/documentation/Software Guidebook/chapters/5_data_structure.md new file mode 100644 index 0000000..8177759 --- /dev/null +++ b/documentation/Software Guidebook/chapters/5_data_structure.md @@ -0,0 +1,17 @@ +# Data structure +In dit hoofdstuk van het softwareguidebook wordt het ERD en de datastore van Redux beschreven. + +#### ERD + +![](../images/erd.jpg) + +In het bovenstaande diagram wordt het entity relationship model getoond. In dit diagram wordt alle informatie die in de database wordt opgeslagen gemodeleerd. We hebben dit database model gemodelleerd aan de hand van document georienteerde manier (MongoDB). + +In dit model zijn er twee collecties gemodelleerd met schema's die embedded in deze collecties staan. We hebben deze twee collecties aan elkaar gekoppeld aan de hand van lessonId in een lessonresult. Zo kan je altijd een lesresultaat vinden aan de hand een les. + + +#### Redux data diagram + +![](../images/redux_state_1.png) + +Er zijn in totaal 6 reducers gemaakt. De afbeelding hierboven is de initiële staat van de store bij de startpagina. \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/6_Kwaliteit_attributen.md b/documentation/Software Guidebook/chapters/6_Kwaliteit_attributen.md new file mode 100644 index 0000000..add1341 --- /dev/null +++ b/documentation/Software Guidebook/chapters/6_Kwaliteit_attributen.md @@ -0,0 +1,22 @@ +# Kwaliteit attributen +In dit hoofdstuk zullen kwaliteitsattributen van de Programmero applicatie beschreven worden. + +## Prestatie +De snelheid waarmee de applicatie op dit moment werkt is niet te berekenen aangezien deze lokaal wordt gedraaid. Dit is dus afhangend van de CPU die lokaal draait. In hoofdstuk 10 van dit software guidebook is een installatie gedaan op de NAS die niet representatief is voor een normale hosting die gedaan kan worden. + +De laadtijd die is vastgesteld op de lokale omgeving is onder de 1 seconde. +De laadtijd die is vastgesteld op de NAS is 15 seconden. + +## Veiligheid +De drie gebruikers die bij functioneel overzicht staan beschreven staan allemaal afgeschermd van elkaar in een eigen omgeving. We maken hier gebruik van JWT, een codebibliotheek die, wanneer je inlogt, een token bewaard in de localstorage van de client. Voor elke request die naar de server wordt gestuurd wordt deze token meegestuurd en vergeleken in de backend van de applicatie. + +## Internationalisatie +De applicatie maakt alleen gebruik van Nederlandse taal en teksten. Er is geen rekening gehouden met meerdere talen ondersteuning. + +## Compatibiliteit +De Programmero appplicatie is gemaakt voor mobiele form factor. De applicatie zal als je de applicatie op je mobiel bezoekt zich aanpassen op de resolutie van het scherm van de client. + +De Programmero appplicatie werkt stabiel op de volgende browsers: +1. Google Chrome +2. Mozilla Firefox +3. Internet Explorer 8 (en nieuwere versies) diff --git a/documentation/Software Guidebook/chapters/7_Limitaties.md b/documentation/Software Guidebook/chapters/7_Limitaties.md new file mode 100644 index 0000000..021324a --- /dev/null +++ b/documentation/Software Guidebook/chapters/7_Limitaties.md @@ -0,0 +1,13 @@ +# Limitaties +In dit hoofdstuk worden alle limitaties waar we in dit project tegenaan lopen neergezet. + +#### Tijd +Programmero wordt gerealiseerd in 8 weken tijd waarvan er 6 weken verdeeld zijn over 3 sprints. De overige twee weken zijn verdeeld in pre-game en post-game voor voorbereiding en afwerking. + +#### Budget +Er is geen budget vastgesteld voor geen enkel onderdeel van de applicatie. Hierbij is het dus niet mogelijk om de applicatie te hosten of externe betaalde tools te gebruiken om de applicatie te realiseren. + +Dit betekent ook dat de applicatie geen gebruik kan maken van betaalde licenties van andere software, bibliotheken of frameworks. + +#### Kennis +Projectgroep aardvark bestaat uit 5 studenten die aan het begin van hun studie zitten (2e jaars). Hierbij is er een limitatie van kennis in de verschillende elementen van de applicatie en zijn technologieën. Er kan geen aansprakelijkheid worden gesteld aan de studenten voor ongedocumenteerde problemen van de applicatie. \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/8_Principes.md b/documentation/Software Guidebook/chapters/8_Principes.md new file mode 100644 index 0000000..f63f792 --- /dev/null +++ b/documentation/Software Guidebook/chapters/8_Principes.md @@ -0,0 +1,38 @@ +# Principes + +## Configuratie +Configuratie is af en toe een lastig aspect van software ontwikkeling. Om alles op te zetten is ons eerste principe dan ook de configuratie die je moet hebben om de applicatie correct te laten werken, maar ook om bijvoorbeeld testing te laten werken. + +Voor configuratie gebruiken we dotenv. Een bibliotheek die we in de `programmero-backend` en `e2e` folder gebruiken om desbetreffende artifacts op te zetten. We gaan ervan uit dat de sleutels die in de `.env.example` zelf ingevuld kunnen worden om de juiste lokale of remote configuratie de applicatie werkend te krijgen. Dit doe je door de `.env.example` te kopieëren en `.env` aan te maken en de juiste gegevens in te vullen. De bedoeling voor `programmero-backend` is hier dat je twee MongoDB databases hebt (een voor productie en een voor testing), app variabelen, een JWT secret key die je kan aamaken in `programmero-backend/scripts/createUserPassword.js` en SMPT gegevens voor de mailing. + +## Geautomatiseerd testen +Een van de andere principes die we willen hanteren is het gebruik van unit tests en end-to-end tests. Hierbij wordt er een aparte omgeving neergezet waar de functionaliteiten van het opgezette artifact worden getest. Met een end-to-end test worden geen individuele functies getest, maar wordt de applicatie doorlopen in een automatische computergestuurde browser die door de applicatie klikt om onderdelen te testen die op elkaar werken volgens een bepaalde volgorde. + +### Unit tests +Het gebruik van unit testing is om te blijven checken of functies die zijn gemaakt werken zoals ze ontworpen zijn. We testen hierbij functies en classes van verschillende onderdelen van de app in beide backend en frontend. + +Om de tests correct uit te voeren moeten er de volgende commando's uitvoerd worden voor hun respectievelijke artifacten. + +**Backend** +``` +$ cd programmero-backend +$ npm i +$ npm test -- --runInBand --silent +``` + +**Frontend** +``` +$ git checkout unittests/frontend +$ cd programmero-frontend +$ npm i +$ npm test +``` + +### End-to-end tests +End-to-end testing is het principe van een UI test die automatisch door de applicatie heen klikt en zo alles test. + +``` +$ cd e2e +$ npm i +$ npm start +``` \ No newline at end of file diff --git a/documentation/Software Guidebook/chapters/9_Software_architectuur.md b/documentation/Software Guidebook/chapters/9_Software_architectuur.md new file mode 100644 index 0000000..92100b2 --- /dev/null +++ b/documentation/Software Guidebook/chapters/9_Software_architectuur.md @@ -0,0 +1,56 @@ +# Software architectuur + +## Context diagram +![alt text](../images/Context_diagram.svg "Context diagram") + +In dit bovenstaande diagram is de context van het Programmero syteem getekend. Hier zijn alle systemen waar de eindgebruiker van de applicatie interactie mee heeft om de applicatie in zijn geheel te laten werken. + +## Container diagram +![alt text](../images/Container_diagram.svg "Container diagram") + +Dit container diagram is een visuele representatie van het hele Programmero systeem. Hier zijn alle interne en externe onderdelen van de applicatie weergegeven om een duidelijk beeld te krijgen hoe elk onderdeel van de applicatie met elkaar werkt op een macro grootte. + +* Web applicatie: Een met React, Redux en Javascript gemaakte front-end website waarop gebruikers acties kunnen uitvoeren. +* Web server: Een Javscript, Express en mongoDB server dat de enige plek is waar gebruikers via het Internet het systeem kunnen bereiken. +* Database: Een MongoDB/Mongoose database die Programmero gebruikt om data op te slaan. +* Mail server: Een Mailtrap.io server die gebruikt wordt om emails vanaf het systeem naar de gebruikers te sturen. + +## Component Front-end diagram +Dit diagram toont de relatie tussen alle componenten binnen het front-end systeem van Programmero. + +### __Let op:__ +* Redux Reducers komen vaker voor en staan aangegeven per kleur. Dit is gedaan omdat meerdere React componenten bepaalde Reducers gebruiken. +* Redux Actions staan niet in het diagram omdat het heel erg lastig is om deze duidelijk te tonen. Een React component kan bijvoorbeeld zonder action bij een reducer data ophalen, maar hij kan ook via een action data schrijven naar een reducer. Onder het diagram staat per React component beschreven welke action deze gebruikt. + +![alt text](../images/Component_diagram_front_end.svg "Component Front-end diagram") + +Dit frontend component diagram laat de gehele werking van de frontend React componenten zien. Frontend bestaat uit 3 begrippen: components, action creators en reducers met elk hun eigen functionalteit. Hierbij zijn: +- Components: User Interface React elementen die onderdelen van de applicatie renderen en acties kunnen uitvoeren. +- Action creators: Worden door components beheerd en aangeroepen, veranderen de state van Redux door reducers aan te roepen. +- Reducers: Functies die informatie uitsluiten. + +#### Action creators per React Component: +* AddQuestion.jsx gebruikt actionCreator(s): questionManagementAction, lessonManagementAction. +* AddPassword.jsx gebruikt actionCreator(s): userActions. +* InviteUser.jsx gebruikt actionCreator(s): userActions. +* LessonManagement.jsx gebruikt actionCreator(s): lessonManagementAction. +* Login.jsx gebruikt actionCreator(s): userActions. +* QuestionEdit.jsx gebruikt actionCreator(s): questionManagementAction, lessonManagementAction. +* QuestionManagement.jsx gebruikt actionCreator(s): questionManagementAction, lessonManagementAction. +* Feedback.jsx gebruikt actionCreator(s): practiceAction, questionManagementAction. +* LessonprogramResult.jsx gebruikt actionCreator(s): userActions. +* Practice.jsx gebruikt actionCreator(s): questionManagementAction, practiceAction. +* StudentProgramms.jsx gebruikt actionCreator(s): practiceAction, questionManagementAction, lessonManagementAction. + +## Component Back-end diagram +Dit diagram toont de relatie tussen alle componenten binnen het back-end systeem van Programmero. + +### __Let op:__ +* Iedere Route gebruikt validationCheckers.js. + +![alt text](../images/Component_diagram_backend.svg "Component Back-end diagram") + +Dit laatste component diagram laat de gehele werking van de backend onderdelen zien. Backend bestaat uit 3 begrippen: Routes, models en middleware met elk hun eigen functionalteit. Hierbij zijn: +- Routes: alle REST endpoints van de server. Hier staan alle URLs waar de frontend van de applicatie aanvragen naar kan versturen. +- Models: de enige functies die toegang hebben tot de database. Deze worden gebruikt in de routes om controle te hebben over de collecties die er in de database staan. +- Middleware: alle functies die voor een request worden uitgevoerd. De enige middleware die we gebruiken is een authorisatie middleware die kijkt of een gebruiker is ingelogd en zet een sessie in de request zelf. \ No newline at end of file diff --git a/documentation/Software Guidebook/images/Capture.JPG b/documentation/Software Guidebook/images/Capture.JPG new file mode 100644 index 0000000..0a733bf Binary files /dev/null and b/documentation/Software Guidebook/images/Capture.JPG differ diff --git a/documentation/Software Guidebook/images/Component_diagram_backend.svg b/documentation/Software Guidebook/images/Component_diagram_backend.svg new file mode 100644 index 0000000..0023499 --- /dev/null +++ b/documentation/Software Guidebook/images/Component_diagram_backend.svg @@ -0,0 +1,2 @@ + +
Programmero
Back-end systeem
<font style="font-size: 22px">Programmero<br>Back-end systeem</font>
HTTP Request
HTTP Request
Front-end systeem
[Container]
React v16.6.3
Redux v5.1.1

[Not supported by viewer]
Database
[Container]

MongoDB v3.1.10
Mongoose v5.3.15
[Not supported by viewer]
Gebruikt
Gebruikt
authorization.js
[Middleware]
[Not supported by viewer]
Gebruikt
Gebruikt
Gebruikt
Gebruikt
lessons.js
[Route : /lessons ]
Verwerkt HTTP requests betreft lessons
[Not supported by viewer]
Gebruikt
Gebruikt
Gebruikt
Gebruikt
users.js
[Route : /users ]
Verwerkt HTTP requests betreft users
[Not supported by viewer]
Gebruikt
Gebruikt
Gebruikt
Gebruikt
lessonresults.js
[Nested Route : /nested/lessonresults ]
lessonresults.js<br>[Nested Route : /nested/lessonresults ]<br>
Gebruikt
Gebruikt
Gebruikt
Gebruikt
codeCards.js
[Nested Route : /nested/codeCards ]
codeCards.js<br>[Nested Route : /nested/codeCards ]<br>
Gebruikt
Gebruikt
Users.js
[Model]
Schrijft data naar en leest data van de database betreft users
[Not supported by viewer]
Gebruikt
Gebruikt
Lessons.js
[Model]
Schrijft data naar en leest data van de database betreft lessons
[Not supported by viewer]
CodeCard.js
[Schema]
Schrijft data naar en leest data van de database betreft CodeCards
[Not supported by viewer]
Schrijft naar en leest van
Schrijft naar en leest van
LessonResult.js
[Schema]
Schrijft data naar en leest data van de database betreft lesson results
[Not supported by viewer]
Gebruikt
Gebruikt
authentication.js
[Route : /auth]
[Not supported by viewer]
\ No newline at end of file diff --git a/documentation/Software Guidebook/images/Component_diagram_front_end.svg b/documentation/Software Guidebook/images/Component_diagram_front_end.svg new file mode 100644 index 0000000..b427a15 --- /dev/null +++ b/documentation/Software Guidebook/images/Component_diagram_front_end.svg @@ -0,0 +1,2 @@ + +
Gebruikt
Gebruikt

AddQuestion.jsx
[React component]
[Not supported by viewer]
authReducer.js
[Redux Reducer]
authReducer.js<br>[Redux Reducer]<br>
Gebruikt
Gebruikt

AddPassword.jsx
[React component]
<br>AddPassword.jsx<br>[React component]
appReducer.js
[Redux Reducer]
appReducer.js<br>[Redux Reducer]
Gebruikt
Gebruikt

InviteUser.jsx
[React component]
<br>InviteUser.jsx<br>[React component]
studentReducer.js
[Redux Reducer]
studentReducer.js<br>[Redux Reducer]
Gebruikt
Gebruikt

LessonManagement.jsx
[React component]
<br>LessonManagement.jsx<br>[React component]
userReducer.js
[Redux Reducer]
userReducer.js<br>[Redux Reducer]
lessonManagement-Reducer.js
[Redux Reducer]
[Not supported by viewer]
questionLis-tReducer.js
[Redux Reducer]
[Not supported by viewer]
authReducer.js
[Redux Reducer]
authReducer.js<br>[Redux Reducer]
Gebruikt
[SMTP]
[Not supported by viewer]
userReducer.js
[Redux Reducer]
userReducer.js<br>[Redux Reducer]
authReducer.js
[Redux Reducer]
authReducer.js<br>[Redux Reducer]
appReducer.js
[Redux Reducer]
appReducer.js<br>[Redux Reducer]
lessonManagement-Reducer.js
[Redux Reducer]
[Not supported by viewer]
Gebruikt
Gebruikt

Login.jsx
[React component]
<br>Login.jsx<br>[React component]
authReducer.js
[Redux Reducer]
authReducer.js<br>[Redux Reducer]
Gebruikt
Gebruikt

QuestionEdit.jsx
[React component]
[Not supported by viewer]
authReducer.js
[Redux Reducer]
authReducer.js<br>[Redux Reducer]<br>
appReducer.js
[Redux Reducer]
appReducer.js<br>[Redux Reducer]
lessonManagement-Reducer.js
[Redux Reducer]
[Not supported by viewer]
authReducer.js
[Redux Reducer]
authReducer.js<br>[Redux Reducer]
Gebruikt
Gebruikt

QuestionManagement.jsx
[React component]
<br>QuestionManagement.jsx<br>[React component]
lessonManagement-Reducer.js
[Redux Reducer]
[Not supported by viewer]
Gebruiker
[Person]


[Not supported by viewer]
HTTP Request
HTTP Request
_combine-Reducers.js
<font style="font-size: 18px">_combine-Reducers.js<br></font>
Back-end systeem
[Container]
Express v4.16.4
MongoDB v3.1.10

[Not supported by viewer]

Feedback.jsx
[React component]
<br>Feedback.jsx<br>[React component]
appReducer.js
[Redux Reducer]
appReducer.js<br>[Redux Reducer]

Lessonprogram-Result.jsx.jsx
[React component]
[Not supported by viewer]
studentReducer.js
[Redux Reducer]
studentReducer.js<br>[Redux Reducer]
studentReducer.js
[Redux Reducer]
studentReducer.js<br>[Redux Reducer]

Practice.jsx
[React component]
<br>Practice.jsx<br>[React component]
appReducer.js
[Redux Reducer]
appReducer.js<br>[Redux Reducer]
studentReducer.js
[Redux Reducer]
studentReducer.js<br>[Redux Reducer]

StudentProgramms.jsx
[React component]
[Not supported by viewer]
appReducer.js
[Redux Reducer]
appReducer.js<br>[Redux Reducer]
lessonManagement-Reducer.js
[Redux Reducer]
[Not supported by viewer]
Gebruikt
Gebruikt
HTTP request
HTTP request
Programmero
Front-end systeem
<font style="font-size: 22px">Programmero<br>Front-end systeem</font>
Mailserver
[External container]

Mailtrap.io
[Not supported by viewer]
\ No newline at end of file diff --git a/documentation/Software Guidebook/images/Container_diagram.JPG b/documentation/Software Guidebook/images/Container_diagram.JPG new file mode 100644 index 0000000..58f47f0 Binary files /dev/null and b/documentation/Software Guidebook/images/Container_diagram.JPG differ diff --git a/documentation/Software Guidebook/images/Container_diagram.svg b/documentation/Software Guidebook/images/Container_diagram.svg new file mode 100644 index 0000000..ca366fa --- /dev/null +++ b/documentation/Software Guidebook/images/Container_diagram.svg @@ -0,0 +1,2 @@ + +
Student
[Person]

Een student die een account bezit
[Not supported by viewer]
Gebruikt
[HTTP, JSON]

[Not supported by viewer]
Front-end systeem
[Container : React v16.6.3
Redux v5.1.1]

Stuurt HTTP REST requests naar het back-end systeem en geeft zo studenten de mogelijkheid om te oefenen en docenten de mogelijkheid om Programmero te beheren.
Ontvangt HTTP  responses van het back-end systeem om deze te tonen aan de gebruikers
[Not supported by viewer]
Leest van en schrijft op
[JSON]
Leest van en schrijft op<br>[JSON]<br>
Verstuurt E-mail door middel van
[SMTP]
Verstuurt E-mail door middel van<br>[SMTP]<br>
Back-end systeem
[Container : Javascript, Express v4.16.4]

Ontvangt HTTP Rest requests van het front-end systeem.
Stuurt queries naar de database om data op te slaan.
Ontvangt query responses van de database.
[Not supported by viewer]
Docent
[Person]

Een docent die een account bezit
[Not supported by viewer]
Gebruikt
[HTTP]
[Not supported by viewer]
Database
[Container : MongoDB v3.1.10 Mongoose v5.3.15]

Ontvangt JSON queries van het back-end systeem
Stuurt de JSON response van de uitgevoerde query.
[Not supported by viewer]
Stuurt e-mail naar
Stuurt e-mail naar
E-mail server
[External container : mailtrap.io]

Ontvangt een SMTP request van het back-end systeem.
Verstuurt een e-mail naar een gebruiker
[Not supported by viewer]
Programmero systeem
[Not supported by viewer]
\ No newline at end of file diff --git a/documentation/Software Guidebook/images/Context_diagram.svg b/documentation/Software Guidebook/images/Context_diagram.svg new file mode 100644 index 0000000..247fc2c --- /dev/null +++ b/documentation/Software Guidebook/images/Context_diagram.svg @@ -0,0 +1,2 @@ + +
Verstuurt e-mails door middel van
<span>Verstuurt e-mails door middel van</span>
Web applicatie
[Front-end systeem]

Voor Docent: Maakt het mogelijk om lesprogramma''s, lessen en vragen te beheren of toe te voegen. Ook kan er een student of docent toegevoegd worden waar naar een invite mail word gestuurd met een activatie code die eenmalig gebruikt kan worden om een account te activeren.

Voor Student: Geeft een student de mogelijkheid om te oefenen per lesprogramma.

[Not supported by viewer]
Verstuurt e-mails naar
Verstuurt e-mails naar
Mail server
[Back-end systeem]

Verstuurt e-mails naar gebruikers.
[Not supported by viewer]
Student
[Person]

Een student die een account bezit
[Not supported by viewer]
Gebruikt
Gebruikt
Docent
[Person]

Een docent die een account bezit
[Not supported by viewer]
Gebruikt
Gebruikt
diff --git a/documentation/Software Guidebook/images/erd.jpg b/documentation/Software Guidebook/images/erd.jpg new file mode 100644 index 0000000..6d3ec0e Binary files /dev/null and b/documentation/Software Guidebook/images/erd.jpg differ diff --git a/documentation/Software Guidebook/images/login.png b/documentation/Software Guidebook/images/login.png new file mode 100644 index 0000000..c677175 Binary files /dev/null and b/documentation/Software Guidebook/images/login.png differ diff --git a/documentation/Software Guidebook/images/redux_state_1.png b/documentation/Software Guidebook/images/redux_state_1.png new file mode 100644 index 0000000..0555074 Binary files /dev/null and b/documentation/Software Guidebook/images/redux_state_1.png differ diff --git a/e2e/example.env b/e2e/example.env new file mode 100644 index 0000000..00aa1c2 --- /dev/null +++ b/e2e/example.env @@ -0,0 +1,10 @@ +HEADLESS = false +SAVESCREENSHOT = TRUE + +FRONTENDURL = http://localhost:3000/ + +ADMINLOGIN = admin@admin.nl +ADMINPASS = test + +STUDENTLOGIN = student@student.nl +STUDENTPASS = test \ No newline at end of file diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..1c1510d --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,22 @@ +{ + "name": "e2e-programmero", + "version": "1.0.0", + "description": "end to end tests of programmero ", + "main": "server.js", + "directories": { + "test": "tests" + }, + "dependencies": { + "dateformat": "^3.0.3", + "dotenv": "^6.2.0", + "jest": "^23.6.0", + "puppeteer": "^1.11.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "author": "", + "license": "ISC" +} diff --git a/e2e/screenshots/admin/lessons/.gitkeep b/e2e/screenshots/admin/lessons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/e2e/screenshots/admin/questions/.gitkeep b/e2e/screenshots/admin/questions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/e2e/server.js b/e2e/server.js new file mode 100644 index 0000000..53f4e33 --- /dev/null +++ b/e2e/server.js @@ -0,0 +1,33 @@ +require('dotenv').config() + +const puppeteer = require("puppeteer"); +const rimraf = require('rimraf'); + +const loginAdmin = require('./tests/login/admin'); +const lessonManagement = require('./tests/lessonManagement'); +const questionManagement = require('./tests/questionManagement'); + +const loginStudent = require('./tests/login/student'); + +rimraf('./screenshots/login/*', function () { console.info('Deleted Login Screenshots'); }); +rimraf('./screenshots/admin/lessons/*', function () { console.info('Deleted Admin Screenshots'); }); +rimraf('./screenshots/admin/questions/*', function () { console.info('Deleted Admin Screenshots'); }); + + +(async () => { + let browser = await puppeteer.launch({ headless: false, slowMo: 20 }) + const adminHome = await loginAdmin(browser) + + + //Admin tests here + await lessonManagement(adminHome) + await questionManagement(adminHome) + + browser.close(); + + browser = await puppeteer.launch({ headless: false }); + const studentHome = await loginStudent(browser); + //Student tests here + + browser.close(); +})(); \ No newline at end of file diff --git a/e2e/tests/lessonManagement.js b/e2e/tests/lessonManagement.js new file mode 100644 index 0000000..51eaa96 --- /dev/null +++ b/e2e/tests/lessonManagement.js @@ -0,0 +1,46 @@ +const dateFormat = require('dateformat'); +const path = "./screenshots/admin/lessons/"; +const exeTime = dateFormat(Date.now(), "yyyy-mm-dd-HH-MM-ss"); +const ext = ".png" + +const screenshotLessonManagement = `${path}LessonManagement${exeTime}${ext}`; +const screenshotLessonManagementAddLesson = `${path}LessonManagementAddLesson${exeTime}${ext}`; +const screenshotLessonManagementEditLesson = `${path}LessonManagementEditLesson${exeTime}${ext}`; +const screenshotLessonManagementEditedLesson = `${path}LessonManagementEditedLesson${exeTime}${ext}`; +const screenshotLessonManagementDeleteLesson = `${path}LessonManagementDeleteLesson${exeTime}${ext}`; +const screenshotLessonManagementDeletedLesson = `${path}LessonManagementDeletedLesson${exeTime}${ext}`; + +module.exports = async function lessonManagement(page){ + // expect("asdf").toEqual("ja") + await page.waitForSelector("#lessonManagement") + await page.click("#lessonManagement") + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLessonManagement }) + await page.waitForSelector("#lessonName") + await page.type("#lessonName", "eenlesmeteenhelelangenaamzodatdezenietconflicteerdmetanderelessen") + await page.waitForSelector("#addLessonButton") + await page.click("#addLessonButton") + + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLessonManagementAddLesson }) + await page.waitForSelector("#lessons> li:last-child > div > label") + await page.click("#lessons> li:last-child > div > label") + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLessonManagementEditLesson }) + await page.waitForSelector("#newName") + await page.type("#newName", "eenlesmeteennieuwelangenaamzodatdezenietconflicteerdmetanderelessen") + await page.waitForSelector("#edit") + await page.click("#edit") + await page.waitFor(200) + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLessonManagementEditedLesson }) + const checkBox = await page.$("#lessons> li:last-child > div > label"); + await page.waitFor(500) + checkBox.click() + await page.waitForSelector("#delete") + await page.click("#delete") + await page.waitFor(1000) + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLessonManagementDeleteLesson }) + await page.waitForSelector("#confirmdelete") + await page.click("#confirmdelete") + await page.waitFor(200) + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLessonManagementDeletedLesson}) + await page.waitForSelector("#home") + await page.click("#home"); +} \ No newline at end of file diff --git a/e2e/tests/login/admin.js b/e2e/tests/login/admin.js new file mode 100644 index 0000000..e0645df --- /dev/null +++ b/e2e/tests/login/admin.js @@ -0,0 +1,19 @@ +const dateFormat = require('dateformat'); +const path = "./screenshots/login/"; +const exeTime = dateFormat(Date.now(), "yyyy-mm-dd-HH-MM-ss"); +const ext = ".png" + +const screenshotLoginPage = `${path}admin-login${exeTime}${ext}`; +const screenshotHomePage = `${path}admin-home${exeTime}${ext}`; + +module.exports = async function loginAdmin(browser) { + const page = await browser.newPage() + await page.goto(process.env.FRONTENDURL) + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLoginPage }) + await page.type("#email", process.env.ADMINLOGIN) + await page.type("#password", process.env.ADMINPASS) + await page.click("#submit") + await page.waitForNavigation() + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotHomePage }) + return page +} \ No newline at end of file diff --git a/e2e/tests/login/student.js b/e2e/tests/login/student.js new file mode 100644 index 0000000..31995a1 --- /dev/null +++ b/e2e/tests/login/student.js @@ -0,0 +1,19 @@ +const dateFormat = require('dateformat'); +const path = "./screenshots/login/"; +const exeTime = dateFormat(Date.now(), "yyyy-mm-dd-HH-MM-ss"); +const ext = ".png" + +const screenshotLoginPage = `${path}student-login${exeTime}${ext}`; +const screenshotHomePage = `${path}student-home${exeTime}${ext}`; + +module.exports = async function loginStudent(browser) { + const page = await browser.newPage() + await page.goto(process.env.FRONTENDURL) + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotLoginPage }) + await page.type("#email", process.env.STUDENTLOGIN) + await page.type("#password", process.env.STUDENTPASS) + await page.click("#submit") + await page.waitForNavigation() + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotHomePage }) + return page +} \ No newline at end of file diff --git a/e2e/tests/questionManagement.js b/e2e/tests/questionManagement.js new file mode 100644 index 0000000..5516220 --- /dev/null +++ b/e2e/tests/questionManagement.js @@ -0,0 +1,86 @@ +const dateFormat = require('dateformat'); +const path = "./screenshots/admin/questions/"; +const exeTime = dateFormat(Date.now(), "yyyy-mm-dd-HH-MM-ss"); +const ext = ".png" + +const screenshotQuestionManagement = `${path}QuestionManagement${exeTime}${ext}`; +const screenshotQuestionManagementAddQuestionTitle = `${path}QuestionManagementAddQuestionTitle${exeTime}${ext}`; +const screenshotQuestionManagementFinishAddCodeCards = `${path}QuestionManagementFinishAddCodeCards${exeTime}${ext}`; +const screenshotQuestionManagementFinishAddCodeCardsSaved = `${path}QuestionManagementFinishAddCodeCardsSaved${exeTime}${ext}`; +const screenshotQuestionManagementEdit = `${path}QuestionManagementEdit${exeTime}${ext}`; +const screenshotQuestionManagementEditSelectAll = `${path}QuestionManagementEditSelectAll${exeTime}${ext}`; +const screenshotQuestionManagementEditDelete = `${path}QuestionManagementEditDelete${exeTime}${ext}`; + + +const codeCard = ["for", "(", "int i = 0;", "i <=5;", "i++;", ")", "{", "console.log", "(", "\"HALLO\"", ");", "}"]; + +module.exports = async function addAndRemoveQuestion(page) { + await page.waitForSelector("#addQuestion"); + await page.click("#addQuestion"); + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagement }) + await page.waitForSelector(".add-question-form"); + await page.click("input#question"); + await page.type("input#question", "Schrijf een for loop die iets print"); + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagementAddQuestionTitle }) + for (let i = 0; i < codeCard.length; i++) { + await page.click("#codecardadd"); + await page.type("#codecardadd", codeCard[i]); + await page.click("svg.fa-plus"); + } + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagementFinishAddCodeCards }) + await page.waitFor(1000); + await page.waitFor("#save-question"); + await page.click("#save-question"); + let button = await page.$("#save-question"); + await page.waitFor(2000); + await button.click(); + await page.waitForSelector(".alert-success"); + await page.waitFor(1000); + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagementFinishAddCodeCardsSaved }) + + //Delete all questions + await page.goto("http://localhost:3000/"); + await page.waitForSelector("a.question-Management"); + await page.click("a.question-Management"); + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagementEdit }) + await page.waitForSelector(".container"); + await page.waitFor(1000); + + await page.waitForSelector("#lessonSelector"); + await page.click("#lessonSelector"); + + let Select = await page.$("select"); + let Option = await Select.getProperties(); + let i = 0; + for (const property of Option.values()) { + i++; + const element = property.asElement(); + if (element && i > 1 && i < 3) { + let hText = await element.getProperty("text"); + let text = await hText.jsonValue(); + let hValue = await element.getProperty("value"); + let optionValue = await hValue.jsonValue(); + await page.select("#lessonSelector", optionValue); + + } + } + await page.click("#lessonSelector"); + + /Somehow moet ik dit 2 keer aanroepen/ + await page.waitFor(2000); + await page.waitForSelector(".selectAll"); + await page.click(".selectAll"); + await page.waitForSelector(".selectAll"); + await page.click(".selectAll"); + + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagementEditSelectAll }) + + await page.waitForSelector(".verwijder-selectie"); + await page.click(".verwijder-selectie"); + + /*Somehow moet ik dit 2 keer aanroepen*/ + await page.waitFor(2000); + if (process.env.SAVESCREENSHOT) await page.screenshot({ path: screenshotQuestionManagementEditDelete }) + await page.waitForSelector(".verwijder-definitief"); + await page.click(".verwijder-definitief"); +} diff --git a/programmero-backend/.env.example b/programmero-backend/.env.example new file mode 100644 index 0000000..3ba4f89 --- /dev/null +++ b/programmero-backend/.env.example @@ -0,0 +1,16 @@ +JWT_SECRET= + +DB_HOST=localhost +DB_PORT=27017 +DB_NAME=programmero + +APP_PORT=4000 + +DB_HOST_TEST_SUITE=localhost +DB_PORT_TEST_SUITE=27017 +DB_NAME_TEST_SUITE=programmero_test + +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USER= +MAIL_PASS= diff --git a/programmero-backend/.eslintrc.json b/programmero-backend/.eslintrc.json new file mode 100644 index 0000000..777cf91 --- /dev/null +++ b/programmero-backend/.eslintrc.json @@ -0,0 +1,48 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018 + }, + "globals": { + "process": true + }, + "rules": { + "indent": [ + "error", + "tab", + { "SwitchCase": 1 } + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "camelcase": [ + "error", + {"properties": "always"} + ], + "no-trailing-spaces": [ + "error", + { "skipBlankLines": true, "ignoreComments": true } + ], + "comma-dangle": [ + "error", + { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "ignore" + } + ] + } +} \ No newline at end of file diff --git a/programmero-backend/app.js b/programmero-backend/app.js new file mode 100644 index 0000000..1ddc500 --- /dev/null +++ b/programmero-backend/app.js @@ -0,0 +1,28 @@ + +const express = require("express"); +const cors = require("cors"); +const bodyParser = require("body-parser"); + +const lessonsRouter = require("./routes/lessons"); +const usersRouter = require("./routes/users"); +const authenticationRouter = require("./routes/authentication"); + +const authentication = require("./routes/middleware/authorization"); + +const app = express(); + +app.use(bodyParser.json()); +app.use(cors({ origin: true, credentials: true })); +app.options("*", cors({ origin: true, credentials: true })); + +// Open routes without authentication +app.use("/auth", authenticationRouter); + +//Middleware +app.use(authentication.ensureAuthenticated); + +//Authenticated routes +app.use("/lessons", lessonsRouter); +app.use("/users", usersRouter); + +module.exports = app; \ No newline at end of file diff --git a/programmero-backend/databaseSeeder.js b/programmero-backend/databaseSeeder.js new file mode 100644 index 0000000..1ccc24e --- /dev/null +++ b/programmero-backend/databaseSeeder.js @@ -0,0 +1,119 @@ +/* eslint-disable no-console */ +require("dotenv").config(); + +const mongoose = require("mongoose"); +const bcrypt = require("bcryptjs"); +const objectId = require("mongodb").ObjectID; +const mongoUtil = require("./utils/mongoUtil"); + +require("./models/Users"); +require("./models/Lessons"); + +var lessonsModel = mongoose.model("Lessons"); +var usersModel = mongoose.model("Users"); + +var PASSWORD = "test"; +var SALT = 10; + +var users; +var lessons; + +/** + * Seeds the database with standard records. Should be used on first startup. + * WARNING: Deletes whole database and seeds it with basic data. This script should only be run on startup of project and testing. + */ +mongoose.set("useCreateIndex", true); + +mongoose.connect(`mongodb://${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`, { useNewUrlParser: true }, async () => { + await mongoUtil.clearDB(); + await setData(); + + await usersModel.insertMany(users); + await lessonsModel.insertMany(lessons); + + console.info("Database seeded!"); + mongoose.connection.close(); +}); + +const setData = async () => { + let hashedPassword = await bcrypt.hash(PASSWORD, SALT); + + users = [ + { + _id: objectId(), + name: "Admin", + email: "admin@admin.nl", + password: hashedPassword, + mailtoken: "", + isAdmin: true + }, + { + _id: objectId(), + name: "Student", + email: "student@student.nl", + password: hashedPassword, + mailToken: "", + isAdmin: false, + score: 0, + lessonResults: [] + } + ]; + + lessons = [ + { + _id: objectId(), + name: "Demo Programma", + description: "A very cool lesson", + programmingLanguage: "Processing", + codeCards: [] + }, + { + _id: objectId(), + name: "Demo Programma 2", + description: "A very cool lesson", + programmingLanguage: "Processing", + codeCards: [ + { + _id: objectId(), + question: "Schrijf een for loop", + number: 0, + parts: [ + "for", + "(int i=0;i<10;i++)", + "{", + "System.out.println", + "(i)", + "}" + ] + }, + { + _id: objectId(), + question: "Schrijf een print", + number: 1, + parts: [ + "System.out.println", + "(", + "Hello console", + ")", + ";" + ] + }, + { + _id: objectId(), + question: "Schrijf een if statement", + number: 2, + parts: [ + "int i", + "= 80;", + "if", + "(i>50)", + "{", + "System.out.println", + "(i)", + "}" + ] + } + ] + } + ]; +}; \ No newline at end of file diff --git a/programmero-backend/index.js b/programmero-backend/index.js new file mode 100644 index 0000000..c9b5157 --- /dev/null +++ b/programmero-backend/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ +"use strict"; + +require("dotenv").config(); + +require("./models/Users"); +require("./models/Lessons"); + +const mongoose = require("mongoose"); +const app = require("./app"); + +/** + * Starts an Express server on given IP and port + * Starts a MongoDB instance on given IP, port and database name +**/ +const server = app.listen(process.env.APP_PORT, async () => { + mongoose.set("useCreateIndex", true); + await mongoose.connect(`mongodb://${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`, { useNewUrlParser: true }); + + console.info("MongoDB started."); + console.info(`Server started on port ${server.address().port}`); +}); \ No newline at end of file diff --git a/programmero-backend/mails/mail.js b/programmero-backend/mails/mail.js new file mode 100644 index 0000000..f64053a --- /dev/null +++ b/programmero-backend/mails/mail.js @@ -0,0 +1,25 @@ +"use strict"; + +const nodemailer = require("nodemailer"); +const fs = require("fs"); +var ejs = require("ejs"); + +module.exports.transport = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT, + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASS + } +}); + +module.exports.compose = (to, subject, templateName, replace) => { + const template = fs.readFileSync("./mails/" + templateName + ".html", { encoding: "utf-8" }); + const html = ejs.render(template, replace); + return { + from: "", + to, + subject, + html + }; +}; \ No newline at end of file diff --git a/programmero-backend/mails/studentRegistration.html b/programmero-backend/mails/studentRegistration.html new file mode 100644 index 0000000..d2cffd2 --- /dev/null +++ b/programmero-backend/mails/studentRegistration.html @@ -0,0 +1,344 @@ + + + + + + Finish registration + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + +
  + + + + + + + + + + + + +
+
+ + + + + + + + + +
+ + + + + + + +
+
+
+

Welcome + to 

+
+
+
+
+
+

Programmero +

+
+
+
+
+
+
+ + + + +
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Start practicing + programming today!

+
+
+
+
+
+

+ Your teacher has assigned the email address + <%= email %> to programmero, + This means you can practice programming the easy way! + Have questions? Get + in touch with our + support team. +

+ + + Made with <3 by programmers for programmers! Start practicing today. Take courses in java, php, + ruby, c# and javascript!
+
+
+ Cheers, +
+ Fritz + van Deventer +

+
+
+
+ + + + +
+ +
+
+
+
+ + + + + + + + + + + + + + +
+
+ +
+
+
 
+
+
+ + + + \ No newline at end of file diff --git a/programmero-backend/models/Lessons.js b/programmero-backend/models/Lessons.js new file mode 100644 index 0000000..4268980 --- /dev/null +++ b/programmero-backend/models/Lessons.js @@ -0,0 +1,31 @@ +"use strict"; + +const mongoose = require("mongoose"); +const objectId = mongoose.Schema.Types.ObjectId; + +const CodeCard = require("./schemas/CodeCard"); + +const lessonSchema = new mongoose.Schema({ + _id: { + type: objectId, + required: true + }, + name: { + type: String, + unique: true, + required: true + }, + description: String, + programmingLanguage: { + type: String, + required: true + }, + codeCards: [CodeCard] + +}, { collection: "lessons" }); + +lessonSchema.methods.getLesson = async function (id) { + return id; +}; + +mongoose.model("Lessons", lessonSchema); \ No newline at end of file diff --git a/programmero-backend/models/Users.js b/programmero-backend/models/Users.js new file mode 100644 index 0000000..6e5bdc0 --- /dev/null +++ b/programmero-backend/models/Users.js @@ -0,0 +1,34 @@ +"use strict"; + +const mongoose = require("mongoose"); +const objectId = mongoose.Schema.Types.ObjectId; + +const LessonResult = require("./schemas/LessonResult"); + +const usersSchema = new mongoose.Schema({ + _id: { + type: objectId, + required: true + }, + name: String, + email: { + type: String, + required: true, + unique: true + }, + password: String, + mailToken: String, + isAdmin: { + type: Boolean, + required: true + }, + score: Number, + lessonResults: [LessonResult] + +}, { collection: "users" }); + +usersSchema.methods.getUser = async function (id) { + return id; +}; + +mongoose.model("Users", usersSchema); \ No newline at end of file diff --git a/programmero-backend/models/schemas/Answer.js b/programmero-backend/models/schemas/Answer.js new file mode 100644 index 0000000..4f03ae5 --- /dev/null +++ b/programmero-backend/models/schemas/Answer.js @@ -0,0 +1,24 @@ +"use strict"; + +const mongoose = require("mongoose"); +const objectId = mongoose.Schema.Types.ObjectId; + +module.exports = new mongoose.Schema({ + _id: objectId, + number: { + type: Number, + required: true + }, + answer: { + type: [String], + required: true + }, + correct: { + type: Boolean, + required: true + }, + score: { + type: Number, + required: true + } +}); \ No newline at end of file diff --git a/programmero-backend/models/schemas/CodeCard.js b/programmero-backend/models/schemas/CodeCard.js new file mode 100644 index 0000000..0acfddb --- /dev/null +++ b/programmero-backend/models/schemas/CodeCard.js @@ -0,0 +1,23 @@ +"use strict"; + +const mongoose = require("mongoose"); +const objectId = mongoose.Schema.Types.ObjectId; + +module.exports = new mongoose.Schema({ + _id: { + type: objectId, + required: true + }, + number: { + type: Number, + required: true + }, + question: { + type: String, + required: true + }, + parts: { + type: [String], + required: true + } +}); diff --git a/programmero-backend/models/schemas/LessonResult.js b/programmero-backend/models/schemas/LessonResult.js new file mode 100644 index 0000000..20daa0e --- /dev/null +++ b/programmero-backend/models/schemas/LessonResult.js @@ -0,0 +1,31 @@ +"use strict"; + +const mongoose = require("mongoose"); +const objectId = mongoose.Schema.Types.ObjectId; + +const Answer = require("./Answer"); + +module.exports = new mongoose.Schema({ + _id: { + type: objectId, + required: true + }, + lessonId: { + type: objectId, + required: true + }, + date: { + type: Date, + default: Date.now, + required: true + }, + score: { + type: Number, + required: true + }, + completed: { + type: Boolean, + required: true + }, + answers: [Answer] +}); \ No newline at end of file diff --git a/programmero-backend/package.json b/programmero-backend/package.json new file mode 100644 index 0000000..5a07998 --- /dev/null +++ b/programmero-backend/package.json @@ -0,0 +1,35 @@ +{ + "name": "aardvark-backend", + "version": "0.1.0", + "description": "Backend of the project for group aardvark", + "private": true, + "main": "index.js", + "jest": { + "setupTestFrameworkScriptFile": "./tests/setup.js" + }, + "scripts": { + "test": "jest", + "start": "node index.js" + }, + "author": "Projectgroup aardvark", + "license": "ISC", + "devDependencies": { + "eslint": "^5.9.0", + "jest": "^23.6.0", + "sinon": "^7.1.1", + "supertest": "^3.3.0" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.4", + "dotenv": "^6.1.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "express-validator": "^5.3.0", + "jsonwebtoken": "^8.4.0", + "mongodb": "^3.1.10", + "mongoose": "^5.3.15", + "nodemailer": "^4.7.0", + "uuid": "^3.3.2" + } +} diff --git a/programmero-backend/routes/authentication.js b/programmero-backend/routes/authentication.js new file mode 100644 index 0000000..c5e8c17 --- /dev/null +++ b/programmero-backend/routes/authentication.js @@ -0,0 +1,124 @@ +"use strict"; + +const express = require("express"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const mongoose = require("mongoose"); + +const { validationResult } = require("express-validator/check"); +const { loginDataValidator, passwordRegisterValidator } = require("./middleware/validationCheckers"); + +const Users = mongoose.model("Users"); + +const authenticationRouter = express.Router(); + +//All return formats are in JSON. + +/** + * Checks if mailtoken exists + * + * @params + * mailtoken: String + */ + +authenticationRouter.get("/:mailToken/check", (req, res) => { + const { mailToken } = req.params; + Users.findOne({ mailToken }) + .then(user => { + if (!user) throw new Error("TOKEN_INVALID"); + else { + return res.json("USER_FOUND"); + } + }) + .catch(err => res.status(400).json({ errors: err.message })); +}); + +/** + * Logs the user in and authenticates. + * Returns a signed webtoken with user information. + * + * @body + * email: String + * password: String + * + * @returns WebToken + */ + +authenticationRouter.post( + "/login", + loginDataValidator, + (req, res) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + Users.findOne({ email: req.body.email }, (err, user) => { + if (err) return res.status(500).json({ errors: err }); + + if (user) { + if (bcrypt.compareSync(req.body.password, user.password)) { + const token = jwt.sign({ + _id: user._id, + name: user.name, + email: user.email, + isAdmin: user.isAdmin + }, + process.env.JWT_SECRET, { + expiresIn: "14d" + }); + + return res.json(token); + } else { + //Bad practice, code in the response doesnt represent a code, but represents a field in the frontend + //TODO: make consistent over backend and make it compatible with frontend + return res.status(400).json({ + error: "Password not correct.", + code: "password" + }); + } + } else { + //Bad practice, code in the response doesnt represent a code, but represents a field in the frontend + //TODO: make consistent over backend and make it compatible with frontend + return res.status(404).json({ + error: "User does not exist", + code: "email" + }); + } + }); + }); + +/** + * Validates passwords and sets password for user with given mailtoken + * + * @body + * pass: String + * passrepeat: String + * mailToken: String + */ + +authenticationRouter.post("/secret", passwordRegisterValidator, (req, res) => { + const { mailToken, pass, passrepeat } = req.body; + + if (pass !== passrepeat) return res.status(400).json({ errors: "PASSWORDS_DO_NOT_MATCH" }); + + Users.findOne({ mailToken }) + .then(user => { + if (!user) throw new Error("TOKEN_INVALID"); + bcrypt.hash(pass, 10, (err, hash) => { + if (err) { + throw new Error("PASSWORD_DOES_NOT_MEET_STANDARDS"); + } else { + user.password = hash; + user.mailToken = ""; + user.save() + .then(() => res.status(200).send()) + .catch(() => res.status(400).send()); + } + }); + }) + .catch(err => res.status(400).json({ "errors": err.message })); +}); + +module.exports = authenticationRouter; \ No newline at end of file diff --git a/programmero-backend/routes/lessons.js b/programmero-backend/routes/lessons.js new file mode 100644 index 0000000..fa0ee81 --- /dev/null +++ b/programmero-backend/routes/lessons.js @@ -0,0 +1,154 @@ +"use strict"; + +const express = require("express"); +const mongoose = require("mongoose"); +const { validationResult } = require("express-validator/check"); +const { lessonDataValidator, lessonIdValidator } = require("./middleware/validationCheckers"); + +const lessonsRouter = express.Router(); +const codecardsRouter = require("./nested/codeCards"); + +const Lessons = mongoose.model("Lessons"); +const objectId = require("mongodb").ObjectID; + +lessonsRouter.use("/:lessonId/codecards", codecardsRouter); + +//All return formats are in JSON. + +/** + * Gets all lessons. + * Returns an array of all lessons. + * + * @returns Array + */ +lessonsRouter.get("/", (req, res) => { + Lessons.find({}, { codeCards: false }, (err, allLessons) => { + if (err) return res.status(500).json({ errors: err }); + + return res.json(allLessons); + }); +}); + +/** + * Gets a specific lesson by ID. + * Returns a full lesson object. + * + * @params lessonId - ObjectId + * + * @returns Object + */ +lessonsRouter.get( + "/:lessonId", + lessonIdValidator, + (req, res) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + Lessons.findOne({ _id: req.params.lessonId }, { codeCards: false }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return res.status(404).json({ errors: "LESSONS_NOT_FOUND" }); + + return res.json(lesson); + }); + }); + +/** + * Creates a new resource for a lesson that can hold multiple codecards. + * Returns the ObjectId of newly created lesson. + * + * @body + * name: String + * description: String + * programmingLanguage: String + * + * @returns String + */ +lessonsRouter.post( + "/", + lessonDataValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + + const lesson = new Lessons({ + _id: objectId(), + name: req.body.name, + description: req.body.description, + programmingLanguage: req.body.programmingLanguage + }); + + lesson.save((err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.status(201).json({ _id: lesson._id }); + }); + }); + +/** + * Updates a lesson by ID. + * Returns updated lesson in form of an object. + * + * @params lessonId - ObjectId + * + * @body + * name: String + * description: String + * programmingLanguage: String + * + * @returns Object + */ +lessonsRouter.put( + "/:lessonId", + lessonIdValidator, + lessonDataValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const newLesson = { + _id: req.params.lessonId, + name: req.body.name, + description: req.body.description, + programmingLanguage: req.body.programmingLanguage + }; + + Lessons.updateOne({ _id: newLesson._id }, newLesson, (err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.json(newLesson); + }); + }); + +/** + * Deletes a lesson by ID. + * Only returns the status of the request. + * + * @params lessonId - ObjectId + * + * @returns Status 204: No content + */ +lessonsRouter.delete( + "/:lessonId", + lessonIdValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + Lessons.deleteOne({ _id: req.params.lessonId }, (err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.status(204).send(); + }); + }); + +module.exports = lessonsRouter; \ No newline at end of file diff --git a/programmero-backend/routes/middleware/authorization.js b/programmero-backend/routes/middleware/authorization.js new file mode 100644 index 0000000..dd25c79 --- /dev/null +++ b/programmero-backend/routes/middleware/authorization.js @@ -0,0 +1,34 @@ +const mongoose = require("mongoose"); +const jwt = require("jsonwebtoken"); + +const Users = mongoose.model("Users"); + +/** + * Authorizes the user and assigns if it is an admin to request. + * Goes to next route or middleware to process request. + * + * @headers + * authorization - String + */ +module.exports.ensureAuthenticated = (req, res, next) => { + const token = req.headers.authorization; + try { + var decoded = jwt.verify(token, process.env.JWT_SECRET); + if (!decoded) return res.status(401).json({ errors: "NO_TOKEN" }); + + } catch (err) { + return res.status(401).json({ errors: err }); + } + + Users.findOne({ _id: decoded._id }, (err, user) => { + if (err) { + return res.status(500).json({ errors: err }); + } + + if (!user) { + return res.status(401).json({ errors: "USER_NOT_FOUND" }); + } + req.user = user; + return next(); + }); +}; \ No newline at end of file diff --git a/programmero-backend/routes/middleware/validationCheckers.js b/programmero-backend/routes/middleware/validationCheckers.js new file mode 100644 index 0000000..9883d7a --- /dev/null +++ b/programmero-backend/routes/middleware/validationCheckers.js @@ -0,0 +1,80 @@ +const { check } = require("express-validator/check"); + +module.exports.registerDataValidator = [ + check("name") + .isString().withMessage("name needs to be a string") + .not().isEmpty().withMessage("name cannot be empty"), + check("email") + .isEmail().withMessage("email needs to be an email") + .not().isEmpty().withMessage("email cannot be empty"), + check("isAdmin") + .isBoolean().withMessage("isAdmin needs to be a boolean") + .not().isEmpty().withMessage("isAdmin cannot be empty") +]; + +module.exports.passwordRegisterValidator = [ + check("mailToken") + .isString().withMessage("mailToken has to be a string") + .not().isEmpty().withMessage("mailToken cannot be empty"), + check("pass") + .isString().withMessage("pass has to be a string") + .not().isEmpty().withMessage("pass cannot be empty"), + check("passrepeat") + .isString().withMessage("pass has to be a string") + .not().isEmpty().withMessage("pass cannot be empty") +]; + +module.exports.loginDataValidator = [ + check("email") + .exists().withMessage("login needs an email") + .not().isEmpty().withMessage("email cannot be empty") + .isEmail(), + check("password") + .exists().withMessage("login needs a password") + .not().isEmpty().withMessage("password cannot be empty") +]; + +module.exports.lessonDataValidator = [ + check("name") + .isString().withMessage("name needs to be a string") + .not().isEmpty().withMessage("name cannot be empty"), + check("description") + .optional({ nullable: true, checkFalsy: true }) + .isString().withMessage("description needs to be a string") + .isLength({ min: 4, max: 100 }).withMessage("description needs to a minimal of 4 and a maximal of 100 characters"), + check("programmingLanguage") + .isString().withMessage("programmingLanguage needs to be a string") + .not().isEmpty().withMessage("programmingLanguage cannot be empty") +]; + +module.exports.codeCardDataValidator = [ + check("question") + .exists().withMessage("codecard needs a question") + .isString().withMessage("question needs to be a string") + .not().isEmpty().withMessage("question cannot be empty"), + check("answer") + .exists().withMessage("codecard needs a answer") + .isArray().withMessage("needs to be an array") +]; + +module.exports.answerDataValidator = [ + check("answer") + .exists().withMessage("lessonresult needs a answer") + .isArray().withMessage("answer needs to be an array") + .not().isEmpty().withMessage("answer cannot be empty"), + check("index") + .exists().withMessage("endpoint needs an index") + .isNumeric().withMessage("index has to be numeric") +]; + +module.exports.lessonIdValidator = [ + check("lessonId") + .isString().withMessage("lessonId has to be a string") + .not().isEmpty().withMessage("lessonId cannot be empty") +]; + +module.exports.codeCardIdValidator = [ + check("codeCardId") + .isString().withMessage("codeCardId has to be a string") + .not().isEmpty().withMessage("codeCardId cannot be empty") +]; \ No newline at end of file diff --git a/programmero-backend/routes/nested/codeCards.js b/programmero-backend/routes/nested/codeCards.js new file mode 100644 index 0000000..6ef9d48 --- /dev/null +++ b/programmero-backend/routes/nested/codeCards.js @@ -0,0 +1,263 @@ +"use strict"; +const express = require("express"); +const mongoose = require("mongoose"); +const utilSet = require("../../utils/utilSet"); +const { validationResult } = require("express-validator/check"); +const { codeCardDataValidator, lessonIdValidator, codeCardIdValidator } = require("../middleware/validationCheckers"); + +//This is a nested router +//SOURCE: https://stackoverflow.com/questions/25260818/rest-with-express-js-nested-router + +const codeCardsRouter = express.Router({ mergeParams: true }); +const Lessons = mongoose.model("Lessons"); + +const objectId = require("mongodb").ObjectID; + +/** + * Gets all codecards in a lesson. + * Returns an array of all codecards. + * + * @params lessonId - ObjectId + * + * @returns Array + */ +codeCardsRouter.get( + "/", + lessonIdValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + Lessons.findOne({ _id: req.params.lessonId }, { codeCards: true }, (err, lesson) => { + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + return (err) ? + res.status(500).json({ errors: err }) : + res.json(lesson.codeCards); + }); + } +); +/** + * Gets a specific codecard from a lesson by the users current progress on the lesson. + * Returns an object with all info of the current lesson and a hussled que + * + * @params lessonId - ObjectId + * + * @returns Object + */ +codeCardsRouter.get( + "/student", + lessonIdValidator, + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const lessonId = req.params.lessonId; + const lessonResult = req.user.lessonResults.filter((element) => element.lessonId.toString() === lessonId && !element.completed)[0]; + + if (!lessonResult) return res.status(404).json({ errors: "LESSONRESULT_NOT_FOUND" }); + + Lessons.findOne({ _id: lessonId }, { codeCards: true }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + if (lesson.codeCards.length <= 0) return res.status(404).json({ errors: "NO_CODECARDS_IN_LESSON" }); + + const codeCard = lesson.codeCards.filter((element) => element.number === lessonResult.answers.length)[0]; + if (!codeCard) return res.status(409).json({ errors: "CODECARD_WRONG_INDEX" }); + + let newparts = codeCard.parts; + for (let i = 0; i < newparts.length; i++) { + let randomValue = utilSet.getRandomInt(newparts.length); + let value = newparts[i]; + let value2 = newparts[randomValue]; + newparts[i] = value2; + newparts[randomValue] = value; + } + + const response = { + _id: codeCard._id, + question: codeCard.question, + parts: newparts, + amountQuestions: lesson.codeCards.length, + indexCurrentQuestion: lessonResult.answers.length + }; + + if (lessonResult.answers.length === lesson.codeCards.length) response.isLast = true; + + return res.json(response); + }); + } +); + +/** + * Gets a specific codecard from a lesson by ID. + * Returns a full codecard object. + * + * @params lessonId - ObjectId + * @params codeCardId - ObjectId + * + * @returns Object + */ +codeCardsRouter.get( + "/:codeCardId", + lessonIdValidator, + codeCardIdValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + Lessons.findOne({ _id: req.params.lessonId }, { codeCards: true }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + + const codeCard = lesson.codeCards.filter((element) => element._id.toString() === req.params.codeCardId)[0]; + return (codeCard) ? res.json(codeCard) : res.status(404).json({ errors: "CODECARD_NOT_FOUND" }); + }); + }); + +/** + * Adds a new codecard in a lesson. + * Returns the ObjectId of newly created codecard. + * + * @params lessonId - ObjectId + * + * @body + * question: String + * answer: [String] + * + * @returns String + */ +codeCardsRouter.post( + "/", + lessonIdValidator, + codeCardDataValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + const lessonId = req.params.lessonId; + if (!objectId.isValid(lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + Lessons.findOne({ _id: lessonId }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + + const codeCard = { + _id: objectId(), + number: lesson.codeCards.length, + question: req.body.question, + parts: req.body.answer + }; + + Lessons.updateOne({ _id: lessonId }, { $push: { codeCards: codeCard } }, (err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.status(201).json({ _id: codeCard._id.toString() }); + }); + }); + }); + +/** + * Updates a specific codecard from a lesson by ID. + * Returns updated codecard in form of an object. + * + * @params lessonId - ObjectId + * @params codeCardId - ObjectId + * + * @body + * number: Number + * question: String + * answer: [String] + * + * @returns Object + */ +codeCardsRouter.put( + "/:codeCardId", + lessonIdValidator, + codeCardIdValidator, + codeCardDataValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + const errors = validationResult(req); + + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const newCodeCard = { + _id: req.params.codeCardId, + question: req.body.question, + parts: req.body.answer + }; + + Lessons.findOne({ _id: req.params.lessonId }, { codeCards: true }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + + const codeCards = lesson.codeCards.map((element) => { + if (element._id.toString() === newCodeCard._id) { + newCodeCard.number = element.number; + return newCodeCard; + } + return element; + }); + + Lessons.updateOne({ _id: req.params.lessonId }, { codeCards: codeCards }, (err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.json(newCodeCard); + }); + }); + }); + +/** + * Deletes a codecard from a lesson by ID. + * Only returns the status of the request. + * + * @params lessonId - ObjectId + * @params codeCardId - ObjectId + * + * @returns Status 204: No content + */ +codeCardsRouter.delete( + "/:codeCardId", + lessonIdValidator, + codeCardIdValidator, + (req, res) => { + if (!req.user.isAdmin) return res.status(401).json({ errors: "NO_PERMISSION" }); + const errors = validationResult(req); + + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + Lessons.findOne({ _id: req.params.lessonId }, { codeCards: true }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + let amountDeleted = 0; + const codeCards = lesson.codeCards.filter((element) => { + if (element._id.toString() !== req.params.codeCardId) { + return element; + } + amountDeleted++; + }); + + if (amountDeleted === 0) { + return res.status(404).json({ errors: "NOTHING_DELETED" }); + } + + Lessons.updateOne({ _id: req.params.lessonId }, { codeCards }, (err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.status(204).send(); + }); + }); + }); + +module.exports = codeCardsRouter; diff --git a/programmero-backend/routes/nested/lessonresults.js b/programmero-backend/routes/nested/lessonresults.js new file mode 100644 index 0000000..5520c4b --- /dev/null +++ b/programmero-backend/routes/nested/lessonresults.js @@ -0,0 +1,179 @@ +"use strict"; +const express = require("express"); +const mongoose = require("mongoose"); +const { validationResult } = require("express-validator/check"); + +const Lessons = mongoose.model("Lessons"); +const Users = mongoose.model("Users"); +const objectId = require("mongodb").ObjectID; + +const { lessonIdValidator, answerDataValidator } = require("../middleware/validationCheckers"); +const calculateScoreModel = require("../../utils/calculateScoreModel"); + +const lessonResultsRouter = express.Router({ mergeParams: true }); + +/** + * Creates a new lessonresult for a user by a given lessonId. + * Returns the ObjectId of newly created lessonresult. + * + * @params + * lessonId - String + * + * @returns Object + */ +lessonResultsRouter.post( + "/:lessonId", + lessonIdValidator, + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const lessonId = req.params.lessonId; + let lessonResult; + + const lessonResults = req.user.lessonResults.filter((element) => element.lessonId.toString() === lessonId); + + if (lessonResults.length > 0) { + for (let i = 0; i < lessonResults.length; i++) { + if (!lessonResults[i].completed) return res.status(201).json({ _id: lessonResults[i]._id.toString() }); + } + } + + lessonResult = { + _id: objectId(), + lessonId: lessonId, + score: 0, + completed: false, + answers: [] + }; + + Lessons.findOne({ _id: lessonId }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + + Users.updateOne({ _id: req.user._id }, { $push: { lessonResults: lessonResult } }, (err) => { + return (err) ? + res.status(500).json({ errors: err }) : + res.status(201).json({ _id: lessonResult._id.toString() }); + }); + }); + }); + +/** + * Adds an answer to a lessonresult from a user. + * Returns an object containing information if the card is correct. + * + * @params + * lessonId - String + * + * @body Object + * answer - [String] + * index: Number + * + * @returns Object + * correct - Boolean + * correctAnswer - [String] + * newScore - Number + * addedScore - Number + */ + +lessonResultsRouter.post( + "/:lessonId/answer", + lessonIdValidator, + answerDataValidator, + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const lessonId = req.params.lessonId; + + if (req.user.lessonResults.length <= 0) res.status(404).json({ errors: "NO_LESSONRESULTS" }); + + const lessonResult = req.user.lessonResults.filter((element) => element.lessonId.toString() === lessonId && !element.completed)[0]; + + if (!lessonResult) return res.status(404).json({ errors: "NO_LESSONRESULT_FOR_LESSON" }); + + Lessons.findOne({ _id: lessonId }, (err, lesson) => { + if (err) return res.status(500).json({ errors: err }); + if (!lesson) return status(404).json({ errors: "LESSON_NOT_FOUND" }); + + const codeCard = lesson.codeCards[req.body.index]; + const result = calculateScoreModel.calculateScore(req.body.answer, codeCard); + + let newScore; + + const newAnswer = { + _id: objectId(), + number: req.body.index, + answer: req.body.answer, + correct: result.correct, + score: result.addedScore + }; + + lessonResult.answers.push(newAnswer); + newScore = lessonResult.score + result.addedScore; + lessonResult.score = newScore; + + if (lesson.codeCards.length === lessonResult.answers.length) lessonResult.completed = true; + + + Users.updateOne({ _id: req.user._id }, { $inc: { score: result.addedScore }, lessonResults: req.user.lessonResults }, (err) => { + if (err) return res.status(500).json({ errors: err }); + + const response = { + correct: result.correct, + correctAnswer: codeCard.parts, + newScore: newScore, + addedScore: result.addedScore + }; + + return res.json(response); + }); + }); + }); + +/** + * Gets lessonresult from last result that user made in a lesson based on given ID + * + * @params + * lessonId: String + * + * @returns Object + */ + +lessonResultsRouter.get("/:lessonId/end", lessonIdValidator, (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const lessonId = req.params.lessonId; + + if (req.user.lessonResults.length <= 0) res.status(404).json({ errors: "NO_LESSONRESULTS" }); + + const lessonResult = req.user.lessonResults.filter((element) => element.lessonId.toString() === lessonId && element.completed)[req.user.lessonResults.length - 1]; + + if (!lessonResult) res.status(404).json({ errors: "NO_LESSONRESULT_FOR_SESSION" }); + + Lessons.findOne({ _id: lessonId }, (err, lesson) => { + if (err) res.status(500).json({ errors: err }); + if (!lesson) res.status(404).json({ errors: "LESSON_NOT_FOUND" }); + + let amountCorrect = 0; + lessonResult.answers.forEach((answer) => { + if (answer.correct) amountCorrect++; + }); + + const response = { + lessonName: lesson.name, + score: lessonResult.score, + amountQuestions: lesson.codeCards.length, + amountCorrect + }; + + return res.json(response); + }); +}); + +module.exports = lessonResultsRouter; \ No newline at end of file diff --git a/programmero-backend/routes/users.js b/programmero-backend/routes/users.js new file mode 100644 index 0000000..e8b31af --- /dev/null +++ b/programmero-backend/routes/users.js @@ -0,0 +1,84 @@ +"use strict"; + +const express = require("express"); +const mongoose = require("mongoose"); +const User = mongoose.model("Users"); +const uuid = require("uuid/v1"); +const mail = require("../mails/mail"); +const userRouter = express.Router(); +const objectId = mongoose.Types.ObjectId; +const lessonResultsRouter = require("./nested/lessonResults"); + +const { lessonIdValidator, registerDataValidator } = require("./middleware/validationCheckers"); +const { validationResult } = require("express-validator/check"); + +userRouter.use("/lessonresults", lessonResultsRouter); + +/** + * Gets the total score of a user + * Returns a Number with the score + * + * @returns Number + */ +userRouter.get("/score", (req, res) => { + return res.json(req.user.score); +}); + +/** + * Gets the total score of a user from a specific lesson + * Returns a Number with the score + * + * @returns Number + */ +userRouter.get("/score/:lessonId", lessonIdValidator, (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!objectId.isValid(req.params.lessonId)) return res.status(422).json({ errors: "OBJECTID_NOT_VALID" }); + + const lessonResult = req.user.lessonResults.filter((element) => element.lessonId.toString() === req.params.lessonId)[0]; + if (!lessonResult) return res.status(404).json({ errors: "LESSONRESULT_NOT_FOUND" }); + + return res.json(lessonResult.score); +}); + +/** + * Creates a new resource for a user that can hold multiple lessonresults. + * Returns the ObjectId of newly created user. + * + * @returns String + */ +userRouter.post("/", registerDataValidator, (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + + const { name, email, isAdmin } = req.body; + const mailToken = uuid(); + let userData = { + _id: objectId(), + name, + email, + mailToken, + isAdmin + }; + + if (!isAdmin) userData.score = 0; + + const user = new User(userData); + + user.save() + .then(user => { + const transport = mail.transport; + const mailMessage = mail.compose(email, "Activate account", "studentRegistration", user); + transport.sendMail(mailMessage, err => { + if (err) { + res.status(500).json({ errors: "EMAIL_NOT_SEND" }); + + } else { + res.status(201).send(); + } + }); + }) + .catch(err => res.status(400).json(err)); +}); + +module.exports = userRouter; \ No newline at end of file diff --git a/programmero-backend/scripts/createJwtSecret.js b/programmero-backend/scripts/createJwtSecret.js new file mode 100644 index 0000000..9584707 --- /dev/null +++ b/programmero-backend/scripts/createJwtSecret.js @@ -0,0 +1,9 @@ +/* eslint-disable no-console */ + +/** + * Creates a new secret for JWT to set in your .env file. + * Execute with node. + * @Source https://github.com/dwyl/learn-json-web-tokens#how-to-generate-secret-key + */ + +console.log(require("crypto").randomBytes(32).toString("hex")); \ No newline at end of file diff --git a/programmero-backend/scripts/createUserPassword.js b/programmero-backend/scripts/createUserPassword.js new file mode 100644 index 0000000..3ec7a71 --- /dev/null +++ b/programmero-backend/scripts/createUserPassword.js @@ -0,0 +1,15 @@ +/* eslint-disable no-console */ + +/** + * Creates a user password for testing purposes. + * Put the created hashed password in the database for your user that you're going to test. + * Execute with node. + * Use "npm rebuild bcrypt --build-from-source" if it gives an error about a lazy symbol + */ + +const bcrypt = require("bcryptjs"); + +const PASSWORD = "test"; +const SALT = 10; + +bcrypt.hash(PASSWORD, SALT, (err, hash) => console.log(hash)); \ No newline at end of file diff --git a/programmero-backend/tests/data/lesson.js b/programmero-backend/tests/data/lesson.js new file mode 100644 index 0000000..044c61a --- /dev/null +++ b/programmero-backend/tests/data/lesson.js @@ -0,0 +1,21 @@ +const objectId = require("mongodb").ObjectID; +const utilSet = require("../../utils/utilSet"); + +const name = utilSet.getRandomString(); + +const codeCards = [ + { + _id: objectId(), + number: 0, + question: "Test question", + parts: ["TestPart1", "TestPart2"] + } +]; + +module.exports = { + _id: objectId(), + name, + description: "Test lesson for automated testing", + programmingLanguage: "TestLanguage", + codeCards +}; \ No newline at end of file diff --git a/programmero-backend/tests/data/user.js b/programmero-backend/tests/data/user.js new file mode 100644 index 0000000..60182fc --- /dev/null +++ b/programmero-backend/tests/data/user.js @@ -0,0 +1,42 @@ +const objectId = require("mongodb").ObjectID; +const jwt = require("jsonwebtoken"); + +const lesson = require("./lesson"); +const utilSet = require("../../utils/utilSet"); + +const user = { + _id: objectId(), + name: "test", + email: utilSet.getRandomString() + "@test.nl", + password: "$2b$10$m6VURTskC1Pp6xcZjfuIQOmF8NgfnzncdROnVMvovXnP/n6ehCFJu", + isAdmin: true, + score: 75, + lessonResults: [ + { + answers: [], + _id: objectId(), + lessonId: lesson._id, + score: 25, + completed: false + }, + { + answers: [], + _id: objectId(), + lessonId: objectId(), + score: 50, + completed: true + } + ] +}; + +const userToken = jwt.sign({ + _id: user._id, + name: user.name, + email: user.email, + isAdmin: user.isAdmin, + score: user.isAdmin +}, +process.env.JWT_SECRET); + +module.exports.data = user; +module.exports.token = userToken; diff --git a/programmero-backend/tests/setup.js b/programmero-backend/tests/setup.js new file mode 100644 index 0000000..92f5b29 --- /dev/null +++ b/programmero-backend/tests/setup.js @@ -0,0 +1,28 @@ +const mongoose = require("mongoose"); +require("dotenv").config(); + +const mongoUtil = require("../utils/mongoUtil"); + +require("../models/Users"); +require("../models/Lessons"); + +beforeAll((done) => { + if (mongoose.connection.readyState === 0) { + mongoose.connect(`mongodb://${process.env.DB_HOST_TEST_SUITE}:${process.env.DB_PORT_TEST_SUITE}/${process.env.DB_NAME_TEST_SUITE}`, + { useNewUrlParser: true }, + (err) => { + if (err) throw Error(err); + done(); + }); + mongoose.set("useCreateIndex", true); + } +}); + +afterEach(async (done) => { + await mongoUtil.clearDB(); + done(); +}); + +afterAll((done) => { + mongoose.disconnect(() => done()); +}); diff --git a/programmero-backend/tests/unit/codeCardRouter.test.js b/programmero-backend/tests/unit/codeCardRouter.test.js new file mode 100644 index 0000000..ede7b19 --- /dev/null +++ b/programmero-backend/tests/unit/codeCardRouter.test.js @@ -0,0 +1,352 @@ +const sinon = require("sinon"); +const mongoose = require("mongoose"); + +const authorization = require("../../routes/middleware/authorization"); +const mongoUtil = require("../../utils/mongoUtil"); +const utilSet = require("../../utils/utilSet"); +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); +var fakeCodeCard = fakeLesson.codeCards[0]; + +const Lessons = mongoose.model("Lessons"); + +var agent; + +var lessonId = fakeLesson._id.toString(); +var codeCardId = fakeCodeCard._id.toString(); + +var randomUrlPart = utilSet.getRandomString(); + +var baseURL = "/lessons/" + lessonId + "/codecards"; +var weirdBaseURL = "/lessons/" + randomUrlPart + "/codecards"; +var emptyBaseURL = "/lessons//codecards"; + +var idURL = baseURL + "/" + codeCardId; +var weirdCodeCardIdURL = baseURL + "/" + randomUrlPart; + +var weirdLessonIdURL = weirdBaseURL + "/" + codeCardId; + +describe("Codecard router should", () => { + + beforeAll((done) => { + const stub = sinon.stub(authorization, "ensureAuthenticated"); + + stub.callsFake((req, res, next) => { + req.user = fakeUser.data; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + done(); + }); + + beforeEach(async (done) => { + const lessonsModel = new Lessons(fakeLesson); + await lessonsModel.save(); + done(); + }); + + describe("GET /lessons/:lessonId/codecards/student", () => { + describe("fail with", () => { + + test("422 UNPROCCESSABLE ENITITY when wrong lessonId has been given", async (done) => { + const response = await agent.get(weirdBaseURL + "/student"); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + + test("404 NOT FOUND when there is no lesson", async (done) => { + await mongoUtil.clearDB(); + const response = await agent.get(baseURL + "/student"); + + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("LESSON_NOT_FOUND"); + done(); + }); + + test("404 NOT FOUND when there are no codecards in the lesson lesson", async (done) => { + await Lessons.updateOne({ _id: fakeLesson._id }, { codeCards: [] }); + const response = await agent.get(baseURL + "/student"); + + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("NO_CODECARDS_IN_LESSON"); + done(); + }); + }); + + describe("succeed with", () => { + test("200 OK when right lessonId is given and student has a lessonResult", async (done) => { + const response = await agent.get(baseURL + "/student"); + expect(response.statusCode).toBe(200); + expect(response.body.question).toBe(fakeCodeCard.question); + expect(response.body.amountQuestions).toBe(fakeLesson.codeCards.length); + expect(response.body.indexCurrentQuestion).toBe(0); + done(); + }); + }); + }); + + describe("GET /lessons/:lessonId/codecards", () => { + describe("succeed with", () => { + test("200 OK when right lessonId has been given and lesson can be found", async (done) => { + const response = await agent.get(baseURL); + + expect(response.statusCode).toBe(200); + expect(response.body.length).toBe(1); + expect(response.body[0]._id).toBe(fakeCodeCard._id.toString()); + expect(response.body[0].parts[0]).toBe(fakeCodeCard.parts[0]); + expect(response.body[0].parts[1]).toBe(fakeCodeCard.parts[1]); + expect(response.body[0].number).toBe(fakeCodeCard.number); + expect(response.body[0].question).toBe(fakeCodeCard.question); + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when no lessonId has been given", async (done) => { + const response = await agent.get(emptyBaseURL); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when weird lessonId has been given", async (done) => { + const response = await agent.get(weirdBaseURL); + + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + }); + }); + + describe("GET /lessons/:lessonId/codecards/:codeCardId", () => { + describe("succeed with", () => { + test("200 OK when right lessonId and codeCardId has been given and codecard can be found", async (done) => { + const response = await agent.get(idURL); + + expect(response.statusCode).toBe(200); + expect(response.body._id).toBe(fakeCodeCard._id.toString()); + expect(response.body.parts[0]).toBe(fakeCodeCard.parts[0]); + expect(response.body.parts[1]).toBe(fakeCodeCard.parts[1]); + expect(response.body.number).toBe(fakeCodeCard.number); + expect(response.body.question).toBe(fakeCodeCard.question); + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESSABLE ENTITY when weird lessonId has been given", async (done) => { + const response = await agent.get(weirdLessonIdURL); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + + test("404 NOT FOUND when weird codeCardId has been given", async (done) => { + const response = await agent.get(weirdCodeCardIdURL); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("CODECARD_NOT_FOUND"); + done(); + }); + + test("404 NOT FOUND no record of codecard is there", async (done) => { + await Lessons.updateOne({ _id: fakeLesson._id }, { codeCards: [] }); + const response = await agent.get(weirdCodeCardIdURL); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("CODECARD_NOT_FOUND"); + done(); + }); + }); + }); + + describe("POST /lessons/:lessonId/codecards", () => { + describe("succeed with", () => { + test("201 CREATED when correct codecard has been provided and has been added to the lesson", async (done) => { + const codeCard = { + question: "What is an automatic test?", + answer: ["this", "is an", "automatic test"] + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(201); + + const postedCodeCard = await Lessons.findOne({ _id: lessonId }).then((lesson) => lesson.codeCards[1]); + + expect(postedCodeCard.question).toBe(codeCard.question); + expect(postedCodeCard.parts.length).toBe(3); + + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when no question has been given", async (done) => { + const codeCard = { + answer: ["this", "is an", "automatic test"] + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when no answer has been given", async (done) => { + const codeCard = { + question: "What is an automatic test?" + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when question has wrong type", async (done) => { + const codeCard = { + question: 0, + answer: ["this", "is an", "automatic test"] + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when answer has wrong type", async (done) => { + const codeCard = { + question: "What is an automatic test?", + answer: 0 + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + }); + }); + + describe("PUT /lessons/:lessonId/codecards/codeCardId", () => { + describe("succeed with", () => { + test("200 OK when correct codecard has been provided and has been edited in the lesson", async (done) => { + const codeCard = { + question: "What is an automatic test?", + answer: ["this", "is an", "automatic test"] + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(200); + + const postedCodeCard = await Lessons.findOne({ _id: lessonId }).then((lesson) => lesson.codeCards[0]); + + expect(postedCodeCard.question).toBe(codeCard.question); + expect(postedCodeCard.parts.length).toBe(3); + + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when no question has been given", async (done) => { + const codeCard = { + answer: ["this", "is an", "automatic test"] + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when no answer has been given", async (done) => { + const codeCard = { + question: "What is an automatic test?" + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when question has wrong type", async (done) => { + const codeCard = { + question: 0, + answer: ["this", "is an", "automatic test"] + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when answer has wrong type", async (done) => { + const codeCard = { + question: "What is an automatic test?", + answer: 0 + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(codeCard); + + expect(response.statusCode).toBe(422); + done(); + }); + }); + }); + + describe("DELETE /lessons/:lessonId/codecards/:codeCardId", () => { + describe("succeed with", () => { + test("204 NO CONTENT when a codecard is deleted", async (done) => { + const response = await agent.delete(idURL); + expect(response.statusCode).toBe(204); + + const deletedCodeCard = await Lessons.findOne({ _id: fakeLesson._id }).then((lesson) => lesson.codeCards[0]); + expect(deletedCodeCard).toBe(undefined); + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when weird lessonId has been given", async (done) => { + const response = await agent.delete(weirdLessonIdURL); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + + test("404 NOT FOUND when weird codeCardId has been given", async (done) => { + const response = await agent.delete(weirdCodeCardIdURL); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("NOTHING_DELETED"); + done(); + }); + }); + }); +}); diff --git a/programmero-backend/tests/unit/codeCardRouterEC.test.js b/programmero-backend/tests/unit/codeCardRouterEC.test.js new file mode 100644 index 0000000..49c6e49 --- /dev/null +++ b/programmero-backend/tests/unit/codeCardRouterEC.test.js @@ -0,0 +1,60 @@ +const sinon = require("sinon"); +const mongoose = require("mongoose"); + +const authorization = require("../../routes/middleware/authorization"); + +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); + +const Lessons = mongoose.model("Lessons"); +var lessonId = fakeLesson._id.toString(); + +var agent; + +var baseURL = "/lessons/" + lessonId + "/codecards"; + +describe("Codecard router edge case", () => { + beforeAll((done) => { + const stub = sinon.stub(authorization, "ensureAuthenticated"); + + // 409 CONFLICT when there is a wrong index on the codecard + stub.onFirstCall().callsFake((req, res, next) => { + req.user = fakeUser.data; + req.user.lessonResults[0].answers = [1, 2, 3, 4, 5]; + return next(); + }); + + // 404 NOT FOUND when there is no lessonResults with given lessonId created for the user + stub.onSecondCall().callsFake((req, res, next) => { + req.user = fakeUser.data; + req.user.lessonResults = []; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + done(); + }); + + beforeEach(async (done) => { + const lessonsModel = new Lessons(fakeLesson); + await lessonsModel.save(); + done(); + }); + + describe("GET /lessons/:lessonId/codecards/student", () => { + describe("fails with", () => { + test("409 CONFLICT when there is a wrong index on the codecard", async (done) => { + const response = await agent.get(baseURL + "/student"); + expect(response.statusCode).toBe(409); + expect(response.body.errors).toBe("CODECARD_WRONG_INDEX"); + done(); + }); + + test("404 NOT FOUND when there is no lessonResults with given lessonId created for the user", async (done) => { + const response = await agent.get(baseURL + "/student"); + expect(response.statusCode).toBe(404); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/programmero-backend/tests/unit/lessonResultsRouter.test.js b/programmero-backend/tests/unit/lessonResultsRouter.test.js new file mode 100644 index 0000000..7322d74 --- /dev/null +++ b/programmero-backend/tests/unit/lessonResultsRouter.test.js @@ -0,0 +1,143 @@ +const sinon = require("sinon"); +const mongoose = require("mongoose"); + +const authorization = require("../../routes/middleware/authorization"); +const mongoUtil = require("../../utils/mongoUtil"); +const utilSet = require("../../utils/utilSet"); +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); + +const lessonId = fakeLesson._id.toString(); + +const Lessons = mongoose.model("Lessons"); + +var agent; + +var baseURL = "/users/lessonresults/" + lessonId; +var weirdBaseURL = "/users/lessonresults/" + utilSet.getRandomString(); + +describe("LessonResult router should", () => { + beforeAll((done) => { + sinon.stub(authorization, "ensureAuthenticated") + .callsFake((req, res, next) => { + req.user = fakeUser.data; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + done(); + }); + + beforeEach(async (done) => { + const lessonsModel = new Lessons(fakeLesson); + await lessonsModel.save(); + done(); + }); + + describe("POST /users/lessonresults/:lessonId", () => { + describe("and succeed with", () => { + test("201 CREATED when endpoint can find current lessonresult", async (done) => { + const response = await agent.post(baseURL); + expect(response.statusCode).toBe(201); + expect(response.body._id).toBe(fakeUser.data.lessonResults[0]._id.toString()); + done(); + }); + }); + + describe("and fail with", () => { + test("422 UNPROCCESSABLE ENTITY when weird lessonId has been given", async (done) => { + const response = await agent.post(weirdBaseURL); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + }); + }); + + describe("POST /users/lessonresults/:lessonId/answer", () => { + describe("and succeed with", () => { + test("200 OK when answer is fully correct", async (done) => { + const request = { + answer: ["TestPart1", "TestPart2"], + index: 0 + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(200); + done(); + }); + }); + + describe("and fail with", () => { + test("404 NOT FOUND when no lessonresult can be found because all lessonresults on lessonID are already completed", async (done) => { + await mongoUtil.clearDB(); + const request = { + answer: ["TestPart1", "TestPart2"], + index: 0 + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("NO_LESSONRESULT_FOR_LESSON"); + done(); + }); + + test("422 UNPROCCESSABLE ENTITY when weird lessonId has been given", async (done) => { + const request = { + answer: ["TestPart1", "TestPart2"], + index: 0 + }; + const response = await agent.post(weirdBaseURL + "/answer").send(request); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + + test("422 UNPROCCESSABLE ENTITY when no answer has been given", async (done) => { + const request = { + index: 0 + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESSABLE ENTITY when no index has been given", async (done) => { + const request = { + answer: [] + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESSABLE ENTITY when empty answer has been given", async (done) => { + const request = { + answer: [], + index: 0 + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESSABLE ENTITY when answer is not a array", async (done) => { + const request = { + answer: 0, + index: 0 + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESSABLE ENTITY when index is not a number", async (done) => { + const request = { + answer: ["TestPart1", "TestPart2"], + index: "" + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(422); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/programmero-backend/tests/unit/lessonResultsRouterEC.test.js b/programmero-backend/tests/unit/lessonResultsRouterEC.test.js new file mode 100644 index 0000000..421ce15 --- /dev/null +++ b/programmero-backend/tests/unit/lessonResultsRouterEC.test.js @@ -0,0 +1,71 @@ +const sinon = require("sinon"); +const mongoose = require("mongoose"); + +const authorization = require("../../routes/middleware/authorization"); +const mongoUtil = require("../../utils/mongoUtil"); + +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); + +const Lessons = mongoose.model("Lessons"); +var lessonId = fakeLesson._id.toString(); + +var agent; + +var baseURL = "/users/lessonresults/" + lessonId; + +describe("lessonResult router edge case", () => { + beforeAll((done) => { + const stub = sinon.stub(authorization, "ensureAuthenticated"); + + stub.callsFake((req, res, next) => { + req.user = fakeUser.data; + req.user.lessonResults = []; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + done(); + }); + + beforeEach(async (done) => { + const lessonsModel = new Lessons(fakeLesson); + await lessonsModel.save(); + done(); + }); + + describe("POST /users/lessonresults/:lessonId", () => { + describe("and succeed with", () => { + test("201 CREATED when no lessonresult is found by user, search for right one", async (done) => { + const response = await agent.post(baseURL); + expect(response.statusCode).toBe(201); + done(); + }); + }); + + describe("and fail with", () => { + test("404 NOT FOUND when no lesson can be found with the idea when no lessonresult is found by user", async (done) => { + await mongoUtil.clearDB(); + const response = await agent.post(baseURL); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("LESSON_NOT_FOUND"); + done(); + }); + }); + }); + + describe("POST /users/lessonresults/:lessonId/answer", () => { + describe("and fail with", () => { + test("404 NOT FOUND when there is no lessonresult", async (done) => { + const request = { + answer: ["TestPart1", "TestPart2"], + index: 0 + }; + const response = await agent.post(baseURL + "/answer").send(request); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("NO_LESSONRESULTS"); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/programmero-backend/tests/unit/lessonRouter.test.js b/programmero-backend/tests/unit/lessonRouter.test.js new file mode 100644 index 0000000..76c7505 --- /dev/null +++ b/programmero-backend/tests/unit/lessonRouter.test.js @@ -0,0 +1,339 @@ +const sinon = require("sinon"); +const mongoose = require("mongoose"); + +const authorization = require("../../routes/middleware/authorization"); +const mongoUtil = require("../../utils/mongoUtil"); +const utilSet = require("../../utils/utilSet"); +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); + +const Lessons = mongoose.model("Lessons"); + +var agent; + +var randomUrlPart = utilSet.getRandomString(); + +var baseURL = "/lessons"; + +var idURL = baseURL + "/" + fakeLesson._id.toString(); +var weirdIdURL = baseURL + "/" + randomUrlPart; + +describe("Lesson router should", () => { + + beforeAll((done) => { + sinon.stub(authorization, "ensureAuthenticated") + .callsFake((req, res, next) => { + req.user = fakeUser.data; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + done(); + }); + + beforeEach(async (done) => { + const lessonsModel = new Lessons(fakeLesson); + await lessonsModel.save(); + done(); + }); + + describe("GET /lessons", () => { + describe("succeed with", () => { + test("200 OK lessons can be found", async (done) => { + const response = await agent.get(baseURL); + + expect(response.statusCode).toBe(200); + expect(response.body.length).toBe(1); + expect(response.body[0]._id).toBe(fakeLesson._id.toString()); + expect(response.body[0].name).toBe(fakeLesson.name); + expect(response.body[0].description).toBe(fakeLesson.description); + expect(response.body[0].programmingLanguage).toBe(fakeLesson.programmingLanguage); + done(); + }); + }); + }); + + describe("GET /lessons/:lessonId", () => { + describe("succeed with", () => { + test("200 OK a lesson can be found", async (done) => { + const response = await agent.get(idURL); + + expect(response.statusCode).toBe(200); + expect(response.body._id).toBe(fakeLesson._id.toString()); + expect(response.body.name).toBe(fakeLesson.name); + expect(response.body.description).toBe(fakeLesson.description); + expect(response.body.programmingLanguage).toBe(fakeLesson.programmingLanguage); + done(); + }); + }); + + describe("fail with", () => { + test("404 NOT FOUND if no lesson can be found because database is empty", async (done) => { + await mongoUtil.clearDB(); + const response = await agent.get(idURL); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("LESSONS_NOT_FOUND"); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when weird lessonId has been given", async (done) => { + const response = await agent.get(weirdIdURL); + + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + }); + }); + + describe("POST /lessons", () => { + describe("succeed with", () => { + test("201 CREATED a full new lesson has been created", async (done) => { + const lesson = { + name: "POST test", + description: "This is a test where we send a POST request to the server", + programmingLanguage: "PostTestLanguage" + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(201); + + const postedLesson = await Lessons.findOne({ _id: response.body._id }).then((lesson) => lesson); + + expect(postedLesson.name).toBe(lesson.name); + expect(postedLesson.description).toBe(lesson.description); + expect(postedLesson.programmingLanguage).toBe(lesson.programmingLanguage); + done(); + }); + + test("201 CREATED a new lesson without description has been created", async (done) => { + const lesson = { + name: "POST test", + programmingLanguage: "PostTestLanguage" + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(201); + + const postedLesson = await Lessons.findOne({ _id: response.body._id }).then((lesson) => lesson); + + expect(postedLesson.name).toBe(lesson.name); + expect(postedLesson.description).toBe(undefined); + expect(postedLesson.programmingLanguage).toBe(lesson.programmingLanguage); + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when no programming language has been given", async (done) => { + const lesson = { + name: "POST test" + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when no name has been given", async (done) => { + const lesson = { + programmingLanguage: "PostTestLanguage" + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when name has wrong type", async (done) => { + const lesson = { + name: 0, + programmingLanguage: "PostTestLanguage" + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when programming language has wrong type", async (done) => { + const lesson = { + name: "POST test", + programmingLanguage: 0 + }; + + const response = await agent.post(baseURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + }); + }); + + describe("PUT /lessons/:lessonId", () => { + describe("succeed with", () => { + test("200 OK a full lesson is edited", async (done) => { + const lesson = { + name: "PUT test", + description: "This is a test where we send a PUT request to the server", + programmingLanguage: "PutTestLanguage" + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(200); + + expect(response.body._id).toBe(fakeLesson._id.toString()); + expect(response.body.name).toBe(lesson.name); + expect(response.body.description).toBe(lesson.description); + expect(response.body.programmingLanguage).toBe(lesson.programmingLanguage); + + const updatedLesson = await Lessons.findOne({ _id: fakeLesson._id }).then((lesson) => lesson); + + expect(updatedLesson._id.toString()).toBe(fakeLesson._id.toString()); + expect(updatedLesson.name).toBe(lesson.name); + expect(updatedLesson.description).toBe(lesson.description); + expect(updatedLesson.programmingLanguage).toBe(lesson.programmingLanguage); + done(); + }); + + test("200 OK a lesson without description is edited", async (done) => { + const lesson = { + name: "PUT test", + programmingLanguage: "PutTestLanguage" + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(200); + + expect(response.body._id).toBe(fakeLesson._id.toString()); + expect(response.body.name).toBe(lesson.name); + expect(response.body.programmingLanguage).toBe(lesson.programmingLanguage); + + const updatedLesson = await Lessons.findOne({ _id: fakeLesson._id }).then((lesson) => lesson); + + expect(updatedLesson._id.toString()).toBe(fakeLesson._id.toString()); + expect(updatedLesson.name).toBe(lesson.name); + expect(updatedLesson.programmingLanguage).toBe(lesson.programmingLanguage); + done(); + }); + }); + + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when weird lessonId has been given", async (done) => { + const lesson = { + name: "PUT test", + description: "This is a test where we send a PUT request to the server", + programmingLanguage: "PutTestLanguage" + }; + + const response = await agent.put(weirdIdURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when no programming language has been given", async (done) => { + const lesson = { + name: "POST test" + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when no name has been given", async (done) => { + const lesson = { + programmingLanguage: "PostTestLanguage" + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when name has wrong type", async (done) => { + const lesson = { + name: 0, + programmingLanguage: "PostTestLanguage" + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + + test("422 UNPROCCESABLE ENTITY when programming language has wrong type", async (done) => { + const lesson = { + name: "POST test", + programmingLanguage: 0 + }; + + const response = await agent.put(idURL) + .set("Accept", "application/json") + .send(lesson); + + expect(response.statusCode).toBe(422); + done(); + }); + }); + }); + + describe("DELETE /lessons/:lessonId", () => { + describe("succeed with", () => { + test("204 NO CONTENT a lesson is deleted", async (done) => { + const response = await agent.delete(idURL); + expect(response.statusCode).toBe(204); + + const deletedLesson = await Lessons.findOne({ _id: fakeLesson._id }).then((lesson) => lesson); + expect(deletedLesson).toBe(null); + done(); + }); + }); + + describe("fail with", () => { + test("422 UNPROCCESABLE ENTITY when weird lessonId has been given", async (done) => { + const response = await agent.delete(weirdIdURL); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/programmero-backend/tests/unit/userRouter.test.js b/programmero-backend/tests/unit/userRouter.test.js new file mode 100644 index 0000000..66a6408 --- /dev/null +++ b/programmero-backend/tests/unit/userRouter.test.js @@ -0,0 +1,57 @@ +const sinon = require("sinon"); + +const utilSet = require("../../utils/utilSet"); + +const authorization = require("../../routes/middleware/authorization"); +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); +const lessonId = fakeLesson._id.toString(); + +var agent; + +var baseURL = "/users"; + +describe("User router should", () => { + beforeAll((done) => { + sinon.stub(authorization, "ensureAuthenticated") + .callsFake((req, res, next) => { + req.user = fakeUser.data; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + + done(); + }); + + describe("GET /users/score", () => { + describe("and succeed with", () => { + test("200 OK when user score can be found", async (done) => { + const response = await agent.get(baseURL + "/score"); + expect(response.statusCode).toBe(200); + expect(response.body).toBe(fakeUser.data.score); + done(); + }); + }); + }); + + describe("GET /users/score/:lessonId", () => { + describe("and succeed with", () => { + test("200 OK when user has lessonresult to get score of", async (done) => { + const response = await agent.get(baseURL + "/score/" + lessonId); + expect(response.statusCode).toBe(200); + expect(response.body).toBe(fakeUser.data.lessonResults[0].score); + done(); + }); + }); + + describe("and fail with", () => { + test("422 UNPROCCESSABLE ENTITY when lessonId is incorrect", async (done) => { + const response = await agent.get(baseURL + "/score/" + utilSet.getRandomString()); + expect(response.statusCode).toBe(422); + expect(response.body.errors).toBe("OBJECTID_NOT_VALID"); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/programmero-backend/tests/unit/userRouterEC.test.js b/programmero-backend/tests/unit/userRouterEC.test.js new file mode 100644 index 0000000..af22369 --- /dev/null +++ b/programmero-backend/tests/unit/userRouterEC.test.js @@ -0,0 +1,46 @@ +const sinon = require("sinon"); +const mongoose = require("mongoose"); + +const authorization = require("../../routes/middleware/authorization"); + +const fakeUser = require("../data/user"); +const fakeLesson = require("../data/lesson"); + +const Lessons = mongoose.model("Lessons"); +var lessonId = fakeLesson._id.toString(); + +var agent; + +var baseURL = "/users/"; + +describe("User router edge case", () => { + beforeAll((done) => { + const stub = sinon.stub(authorization, "ensureAuthenticated"); + + stub.onCall(0).callsFake((req, res, next) => { + req.user = fakeUser.data; + req.user.lessonResults = []; + return next(); + }); + + agent = require("supertest").agent(require("../../app")); + done(); + }); + + beforeEach(async (done) => { + const lessonsModel = new Lessons(fakeLesson); + await lessonsModel.save(); + done(); + }); + + describe("GET /users/score/:lessonId", () => { + describe("fails with", () => { + test("404 NOT FOUND when there is no lessonresult to be found to show score of", async (done) => { + const response = await agent.get(baseURL + "/score/" + lessonId); + expect(response.statusCode).toBe(404); + expect(response.body.errors).toBe("LESSONRESULT_NOT_FOUND"); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/programmero-backend/utils/calculateScoreModel.js b/programmero-backend/utils/calculateScoreModel.js new file mode 100644 index 0000000..a2b06cc --- /dev/null +++ b/programmero-backend/utils/calculateScoreModel.js @@ -0,0 +1,19 @@ +module.exports.calculateScore = (answer, codeCard) => { + let addedScore; + let amountGood = 0; + const flatScoreMultiplier = 20; + let correct = false; + + for (let i = 0; i < answer.length; i++) { + if (answer[i] === codeCard.parts[i]) amountGood++; + } + + addedScore = amountGood * flatScoreMultiplier; + + if (codeCard.parts.length === amountGood) { + addedScore = addedScore + 100; + correct = true; + } + + return { addedScore, correct }; +}; diff --git a/programmero-backend/utils/mongoUtil.js b/programmero-backend/utils/mongoUtil.js new file mode 100644 index 0000000..beb8f33 --- /dev/null +++ b/programmero-backend/utils/mongoUtil.js @@ -0,0 +1,7 @@ +const mongoose = require("mongoose"); + +module.exports.clearDB = async () => { + for (var i in mongoose.connection.collections) { + await mongoose.connection.collections[i].deleteMany(); + } +}; \ No newline at end of file diff --git a/programmero-backend/utils/utilSet.js b/programmero-backend/utils/utilSet.js new file mode 100644 index 0000000..8670c18 --- /dev/null +++ b/programmero-backend/utils/utilSet.js @@ -0,0 +1,11 @@ +module.exports.getRandomString = () => { + var chars = "abcdefghijklmnopqrstuvwxyz1234567890"; + var randomString = ""; + + for (var i = 0; i < 15; i++) { + randomString += chars[Math.floor(Math.random() * chars.length)]; + } + return randomString; +}; + +module.exports.getRandomInt = (max) => Math.floor(Math.random() * Math.floor(max)); \ No newline at end of file diff --git a/programmero-frontend/.eslintrc.json b/programmero-frontend/.eslintrc.json new file mode 100644 index 0000000..2eaa268 --- /dev/null +++ b/programmero-frontend/.eslintrc.json @@ -0,0 +1,56 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "indent": [ + "error", + "tab", + { "SwitchCase": 1 } + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "camelcase": [ + "error", + {"properties": "always"} + ], + "no-unused-vars": [ + 0, + { "varsIgnorePattern": "^h$" } + ], + "no-trailing-spaces": [ + "error", + { "skipBlankLines": true, "ignoreComments": true } + ], + "comma-dangle": [ + "error", + { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "ignore" + } + ] + } +} \ No newline at end of file diff --git a/programmero-frontend/.gitignore b/programmero-frontend/.gitignore new file mode 100644 index 0000000..461a487 --- /dev/null +++ b/programmero-frontend/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +__snapshots__/ \ No newline at end of file diff --git a/programmero-frontend/README.md b/programmero-frontend/README.md new file mode 100644 index 0000000..31ddba7 --- /dev/null +++ b/programmero-frontend/README.md @@ -0,0 +1 @@ +# Programmero frontend \ No newline at end of file diff --git a/programmero-frontend/package.json b/programmero-frontend/package.json new file mode 100644 index 0000000..29e3d27 --- /dev/null +++ b/programmero-frontend/package.json @@ -0,0 +1,53 @@ +{ + "name": "programmero-frontend", + "version": "0.1.0", + "description": "Frontend of the project for group aardvark", + "private": true, + "author": "Projectgroup aardvark", + "license": "ISC", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.8", + "@fortawesome/free-solid-svg-icons": "^5.5.0", + "@fortawesome/react-fontawesome": "^0.1.3", + "bootstrap": "^4.1.3", + "giphy-random": "^2.0.8", + "global": "^4.3.2", + "jwt-decode": "^2.2.0", + "react": "^16.6.3", + "react-circular-progressbar": "^1.0.0", + "react-dom": "^16.6.3", + "react-dragula": "^1.1.17", + "react-redux": "^5.1.1", + "react-router-dom": "^4.3.1", + "react-scripts": "^2.1.3", + "reactstrap": "^6.5.0", + "redux": "^4.0.1", + "redux-thunk": "^2.3.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "babel-jest": "^23.6.0", + "enzyme": "^3.8.0", + "enzyme-adapter-react-16": "^1.7.1", + "enzyme-to-json": "^3.3.5", + "eslint": "5.6.0", + "eslint-plugin-react": "^7.11.1", + "jest": "^23.6.0" + }, + "jest": { + "snapshotSerializers": [ + "enzyme-to-json/serializer" + ] + } +} diff --git a/programmero-frontend/public/favicon.ico b/programmero-frontend/public/favicon.ico new file mode 100644 index 0000000..2ba8cf8 Binary files /dev/null and b/programmero-frontend/public/favicon.ico differ diff --git a/programmero-frontend/public/index.html b/programmero-frontend/public/index.html new file mode 100644 index 0000000..5d5b9cc --- /dev/null +++ b/programmero-frontend/public/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + Programmero + + + +
+ + + diff --git a/programmero-frontend/public/manifest.json b/programmero-frontend/public/manifest.json new file mode 100644 index 0000000..1f2f141 --- /dev/null +++ b/programmero-frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/programmero-frontend/src/__tests__/components/pages/Feedback.test.js b/programmero-frontend/src/__tests__/components/pages/Feedback.test.js new file mode 100644 index 0000000..a3e340d --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/Feedback.test.js @@ -0,0 +1,52 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { Feedback } from "../../../components/student/Feedback"; + +const history = { push: jest.fn() }; + +let component; + +describe("Feedback Component", () => { + let wrapper; + + it("should render correctly in \"debug\" mode", () => { + component = shallow( + + ); + }); + + it("Test if studentAnswers are loaded into ul properly", () => { + wrapper = shallow( + + ); + + expect( + wrapper.find(".feedbackSection ul > li").length + ).toBeGreaterThanOrEqual(1); + }); + + it("Test if correctAnswer is loaded into ul properly", () => { + wrapper = shallow( + + ); + + expect( + wrapper.find(".correctAnswer ul > li").length + ).toBeGreaterThanOrEqual(1); + }); + + it("Test if the question is loaded in properly", () => { + wrapper = shallow( + + ); + + expect( + wrapper.find(".questionFeedback").text().length + ).toBeGreaterThanOrEqual(0); + }); +}); + diff --git a/programmero-frontend/src/__tests__/components/pages/InviteUser.test.js b/programmero-frontend/src/__tests__/components/pages/InviteUser.test.js new file mode 100644 index 0000000..f96c572 --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/InviteUser.test.js @@ -0,0 +1,48 @@ +import React from "react"; +import { shallow, mount } from "enzyme"; +import { InviteUser } from "../../../components/pages/InviteUser"; + + +// Zet
en components in comentaar voordat je verder gaat. Dit is om de tests te laten slagen. + +let component; +let props = { + match:{ + path:"user" + } +}; +describe("test launch", () => { + component = mount(); + test("expect button to be disabled", () => { + const button = component.find("button"); + expect(button.props().disabled).toEqual(true); + }); + test("expect button to be enabled when inputs are villed and no errors occur", () => { + component.setState({ + emailInput: { + value: "aaaaa" + }, + nameInput: { + value: "aaaaa" + } + }); + const button = component.find("button"); + expect(button.props().disabled).toEqual(false); + }); + + test("expect button to be disabled when error messages occur", () => { + const emailInput = component.find("input[name='Email']"); + emailInput.prop("onChange")({ target: { value: "" } }); + const button = component.find("button"); + expect(component.state().emailInput.errormsg).toEqual( + "Dit invoerveld is leeg" + ); + expect(button.props().disabled).toEqual(false); + }); + + test("expect isBeheerder to be true after clicking on the checkbox", () => { + const isAdminCheckbox = component.find("input[name='CustomCheckbox']"); + isAdminCheckbox.prop("onClick")(); + expect(component.state().isAdmin).toEqual(true); + }); +}); diff --git a/programmero-frontend/src/__tests__/components/pages/Invited.test.js b/programmero-frontend/src/__tests__/components/pages/Invited.test.js new file mode 100644 index 0000000..0482461 --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/Invited.test.js @@ -0,0 +1,13 @@ +import React from "react"; +import { shallow } from "enzyme"; +import Invited from "../../../components/Inviting/Invited"; + +let component; + +describe("Invited Component", () => { + let wrapper; + + it("should render correctly in \"debug\" mode", () => { + component = shallow(); + }); +}); \ No newline at end of file diff --git a/programmero-frontend/src/__tests__/components/pages/LessonManagement.test.js b/programmero-frontend/src/__tests__/components/pages/LessonManagement.test.js new file mode 100644 index 0000000..24542ab --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/LessonManagement.test.js @@ -0,0 +1,50 @@ +import React from "react"; +import { shallow, render, mount } from "enzyme"; +import { LessonManagement } from "../../../components/pages/LessonManagement"; + +// Zet
en components in comentaar voordat je verder gaat. Dit is om de tests te laten slagen. + +let props; +const getLessonsAction = () => {}; + +describe("Lesson Mangemnet component", () => { + let wrapper; + beforeEach(() => { + props = { + getLessonsAction, + active: [1, 2], + lessons: [ + { + _id: "2", + name: "JAVA", + description: "JAVA", + programmingLanguage: "Processing", + __v: 0 + }, + { + _id: "1", + name: "SPD", + description: "SPD", + programmingLanguage: "Processing", + __v: 0 + } + ] + }; + }); + it("Check if the first label in the ul is same as the first element in the given array", () => { + wrapper = mount(); + expect(wrapper.find("label").first().text()).toEqual(" JAVA"); + }); + + it("should render correctly in \"debug\" mode", () => { + wrapper = shallow(); + }); + + it("Check if the lenght of active is the same as given", () => { + wrapper = shallow(); + expect(wrapper.instance().props.active.length).toEqual(2); + }); + it("check if the state showWarningPopup is false", () => { + expect(wrapper.state().showWarningPopup).toEqual(false); + }); +}); diff --git a/programmero-frontend/src/__tests__/components/pages/Login.test.js b/programmero-frontend/src/__tests__/components/pages/Login.test.js new file mode 100644 index 0000000..b863f0f --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/Login.test.js @@ -0,0 +1,28 @@ +import React from "react"; +import { shallow } from "enzyme"; +import {Login} from "../../../components/pages/Login"; + +const history = {push: jest.fn()}; + +let component; + +describe("Test if loggin page loads under the following circumstances:", () => { + it("should render correctly in \"debug\" mode", () => { + component = shallow(); + }); + + it("Redirect if logged in", () => { + component = shallow(); + expect(history.push).toHaveBeenCalled(); + }); + + it("Don't loggin under false credentials", () => { + component = shallow(); + component.find("button").simulate("click"); + }); + + afterEach(() => { + expect(component).toMatchSnapshot(); + component.unmount(); + }); +}); \ No newline at end of file diff --git a/programmero-frontend/src/__tests__/components/pages/Practice.test.js b/programmero-frontend/src/__tests__/components/pages/Practice.test.js new file mode 100644 index 0000000..58b2fcb --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/Practice.test.js @@ -0,0 +1,37 @@ +import React from "react"; +import { shallow } from "enzyme"; +import {Practice} from "../../../components/student/Practice"; + +const history = {push: jest.fn()}; + +let component; +let props = { + match:{ + params:{ + id:1 + } + }, + answers:["1","2"], + question:"test", + answer:["if", "test"], + getCodeCardAction: function(){}, + getScoreAction: function(){} +}; +describe("Practice Component", () => { + let wrapper; + + it("Test if question works", () => { + wrapper = shallow(); + expect(wrapper.find("h4").text()).toEqual("test"); + }); + + it("Test if answers are loaded into the code parts section", () => { + wrapper = shallow(); + + expect(wrapper.find("ul > li").length).toEqual(2); + }); + + it("should render correctly in \"debug\" mode", () => { + component = shallow(); + }); +}); diff --git a/programmero-frontend/src/__tests__/components/pages/QuestionManagement.test.js b/programmero-frontend/src/__tests__/components/pages/QuestionManagement.test.js new file mode 100644 index 0000000..059337e --- /dev/null +++ b/programmero-frontend/src/__tests__/components/pages/QuestionManagement.test.js @@ -0,0 +1,75 @@ +import React from "react"; +import { shallow } from "enzyme"; + +import { QuestionManagement } from "../../../components/pages/QuestionManagement"; +import { getLessonsAction } from "../../../actions/lessonManagementAction"; + +let component; + + +describe("test launch", () => { + + it("should render correctly", () => { + component = shallow(); + }); + + it("expect lessons to be loaded", () => { + component = shallow(); + const optionContainer = component.find("#lessonSelector"); + expect(optionContainer.props().children).toBeDefined(); + }); + + it("expect questions to be showing", () => { + let questions = [ + { + parts: [], + _id: "5c1394c925869607742a7714", + question: "aaaaaa", + number: 0 + }, + { + parts: [], + _id: "5c1394c925869607742a7712", + question: "aaa", + number: 1 + } + ]; + component = shallow(); + const question1 = component.find(".aaaaaa"); + expect(question1.props().children[0]).toBeDefined(); + }); + + it("expect warningpop-up to show when in the correct state", () => { + let questions = [ + { + parts: [], + checked: true, + _id: "5c1394c925869607742a7714", + question: "aaaaaa", + number: 0 + }, + { + parts: [], + checked: true, + _id: "5c1394c925869607742a7712", + question: "aaa", + number: 1 + } + ]; + component = shallow(); + component.setState({questionstoDelete:questions,"showWarningPopup": true}); + const confirmButton = component.find("#pop-up"); + expect(confirmButton.props().isOpen).toEqual(true); + }); + afterEach(() => { + component.unmount(); + }); +}); diff --git a/programmero-frontend/src/__tests__/components/parts/UserHeader.test.js b/programmero-frontend/src/__tests__/components/parts/UserHeader.test.js new file mode 100644 index 0000000..fca744a --- /dev/null +++ b/programmero-frontend/src/__tests__/components/parts/UserHeader.test.js @@ -0,0 +1,24 @@ +import React from "react"; +import { shallow } from "enzyme"; + +import { UserHeader } from "../../../components/parts/UserHeader"; + +let component; + +describe("Test if userheader page loads under the following circumstances:", () => { + it("should render correctly in \"debug\" mode", () => { + component = shallow(); + }); + + it("should logout if user clicks logout", () => { + const name = "kees"; + component = shallow(); + + expect(component.find("#name").text()).toBe(name); + }); + + afterEach(() => { + expect(component).toMatchSnapshot(); + component.unmount(); + }); +}); \ No newline at end of file diff --git a/programmero-frontend/src/actions/actionTypes.js b/programmero-frontend/src/actions/actionTypes.js new file mode 100644 index 0000000..f60960e --- /dev/null +++ b/programmero-frontend/src/actions/actionTypes.js @@ -0,0 +1,43 @@ +//ANSWER TYPES +export const ADD_QUESTION = "add-question"; +export const ADD_ANSWER = "add-answer"; +export const EDIT_ANSWER = "edit-answer"; +export const DELETE_ANSWER = "delete-answer"; +export const GET_CORRECT_ANSWER = "get-correct-answer"; + +//QUESTION TYPES +export const SELECT_ALL_QUESIONS = "select-all-questions"; +export const SET_CHECKBOXES_STATE = "set-checkboxes-state"; +export const CHANGE_CHECKBOX_STATE = "change-checkbox-state"; +export const DELETE_SELECTED_QUESTION = "delete-selected-question"; +export const GET_QUESTION = "get-question"; +export const GET_SPECIFIC_QUESTION = "get-specific-question"; +export const REFRESH_THE_REDUCER = "refresh-the-reducer"; +export const QUESTION_IN_EDITING = "question-in-editing"; +export const ADD_ONE_QUESTION = "one-question"; + +//LESSON TYPES +export const GET_LESSONS = "get-lessons"; +export const CREATE_LESSON = "create-lesson"; +export const SWITCH_LESSON_SELECTION = "switch-lesson-selection"; +export const DELETE_SELECTED_LESSONS = "delete-selected-lessons"; +export const CHANGE_SELECTED_LESSON_NAME = "change-selected-lession-name"; +export const RESET_STATUS_CODE = "reset-status-code"; +export const GET_SCORE = "get-score"; +export const GET_LESSONPROGRAM_SCORE = "get-lessonprogram-score"; +export const GET_LESSON = "get-lesson"; + +//LESSONRESULT TYPES +export const GET_END_LESSONRESULT_INFO = "get-end-lessonresult-info"; + +//USER TYPES +export const SET_USER = "set-user"; + +//STUDENT TYPE +export const GET_CODE_CARD = "get-cards"; +export const CREATE_USER = "create-user"; +export const FILL_USER_PASSWORD = "fill-user-password"; +export const RESET_USER_REDUCER = "reset-user-reducer"; + +//COMMON TYPES +export const ERROR = "error"; diff --git a/programmero-frontend/src/actions/lessonManagementAction.js b/programmero-frontend/src/actions/lessonManagementAction.js new file mode 100644 index 0000000..47848c2 --- /dev/null +++ b/programmero-frontend/src/actions/lessonManagementAction.js @@ -0,0 +1,111 @@ +import { + SWITCH_LESSON_SELECTION, + DELETE_SELECTED_LESSONS, + CHANGE_SELECTED_LESSON_NAME, + GET_LESSONS, + CREATE_LESSON, + RESET_STATUS_CODE, + ERROR +} from "./actionTypes"; +import { + getLessons, + createLesson, + deleteLesson, + updateLesson +} from "../data/lessonsData"; + +export const createLessonAction = ( + name, + description = null, + programmingLanguage = null +) => { + if (description === null) description = "A very cool lesson"; + if (programmingLanguage === null) programmingLanguage = "Processing"; + + const createBody = { name, description, programmingLanguage }; + return dispatch => { + createLesson(createBody) + .then(data => { + dispatch({ + type: CREATE_LESSON, + _id: data._id, + ...createBody, + errorCode: data.status + }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message ? "Server kon niet reageren":error.message + }); + }); + }; +}; + +export const switchLessonSelection = name => { + return { + type: SWITCH_LESSON_SELECTION, + name + }; +}; + +export const deleteSelectedLessons = (lessonId) => { + return dispatch => { + deleteLesson(lessonId).then(result => { + dispatch({ type: DELETE_SELECTED_LESSONS, errorCode: result }); + }).catch(error => { + dispatch({ + type: ERROR, + error: error.message ? "Server kon niet reageren":error.message + }); + }); + }; +}; + +export const changeSelectedLessonName = (lessons, oldName, newName) => { + const lesson = lessons.find(element => { + return element.name === oldName; + }); + + const lessonId = lesson._id; + + return dispatch => { + updateLesson(lessonId, { ...lesson, name: newName }) + .then(result => { + dispatch({ + type: CHANGE_SELECTED_LESSON_NAME, + oldName, + newName, + errorCode: result + }); + }) + .catch(error => { + + dispatch({ + type: ERROR, + error: error.message ? "Server kon niet reageren":error.message + }); + }); + }; +}; + +export const getLessonsAction = () => { + return dispatch => { + getLessons() + .then(data => { + dispatch({ type: GET_LESSONS, lessons: data }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message ? "Server kon niet reageren":error.message + }); + }); + }; +}; + +export const resetStatusCodeAction = () => { + return { + type: RESET_STATUS_CODE + }; +}; diff --git a/programmero-frontend/src/actions/practiceAction.js b/programmero-frontend/src/actions/practiceAction.js new file mode 100644 index 0000000..bbc1bf8 --- /dev/null +++ b/programmero-frontend/src/actions/practiceAction.js @@ -0,0 +1,85 @@ +import { GET_CODE_CARD, ERROR, GET_CORRECT_ANSWER, GET_SCORE, GET_LESSONPROGRAM_SCORE, GET_LESSON, GET_END_LESSONRESULT_INFO } from "./actionTypes"; +import { getCodeCard, getTheScore, getLessonprogramScore, getLesson, getEndLesson } from "../data/practice"; + +export const getCodeCardAction = lessonId => { + return dispatch => { + getCodeCard(lessonId) + .then(data => { + dispatch({ type: GET_CODE_CARD, data }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message?error.message:"Server heeft niet gereageerd " + }); + }); + }; +}; + +export const getTheAnswers = (data, studentAnswer) => { + return { + type: GET_CORRECT_ANSWER, + data, + studentAnswer + }; +}; + +export const getScoreAction = () => { + return dispatch => { + getTheScore() + .then(data => { + dispatch({ type: GET_SCORE, data }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message?error.message:"Server heeft niet gereageerd " + }); + }); + }; +}; + +export const getLessonprogramScoreAction = (lessonId) => { + return dispatch => { + getLessonprogramScore(lessonId) + .then(data => { + dispatch({ type: GET_LESSONPROGRAM_SCORE, data }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message?error.message:"Server heeft niet gereageerd " + }); + }); + }; +}; + +export const getLessonAction = (lessonId) => { + return dispatch => { + getLesson(lessonId) + .then(data => { + dispatch({ type: GET_LESSON, data }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message?error.message:"Server heeft niet gereageerd " + }); + }); + }; +}; + +export const getEndLessonAction = (lessonId) => { + return dispatch => { + getEndLesson(lessonId) + .then(data => { + dispatch({ type: GET_END_LESSONRESULT_INFO, data }); + }) + .catch(error => { + dispatch({ + type: ERROR, + error: error.message?error.message:"Server heeft niet gereageerd " + }); + }); + }; +}; diff --git a/programmero-frontend/src/actions/questionManagementAction.js b/programmero-frontend/src/actions/questionManagementAction.js new file mode 100644 index 0000000..cb4429f --- /dev/null +++ b/programmero-frontend/src/actions/questionManagementAction.js @@ -0,0 +1,117 @@ +import { + ADD_QUESTION, + ADD_ANSWER, + EDIT_ANSWER, + DELETE_ANSWER, + GET_SPECIFIC_QUESTION, + SELECT_ALL_QUESIONS, + SET_CHECKBOXES_STATE, + CHANGE_CHECKBOX_STATE, + DELETE_SELECTED_QUESTION, + GET_QUESTION, + REFRESH_THE_REDUCER, + QUESTION_IN_EDITING, + ERROR, + ADD_ONE_QUESTION +} from "./actionTypes"; +import { questions, question } from "../data/questionData"; + +export const addQuestion = (question) => { + return { + type: ADD_QUESTION, + question + }; +}; + +export const addAnswer = (answer, id = "") => { + return { + type: ADD_ANSWER, + answer, + id + }; +}; +export const inEditing = (answerId) => { + return { + type: EDIT_ANSWER, + answerId + }; +}; +export const deleteAnswer = (id) => { + return { + type: DELETE_ANSWER, + id + }; +}; + +export const selectAllQuestions = (checkboxesState, checkBoxes) => { + return { + type: SELECT_ALL_QUESIONS, + checkboxesState, + checkBoxes + }; +}; + +export const setCheckboxesState = checkboxesState => { + return { + type: SET_CHECKBOXES_STATE, + checkboxesState + }; +}; + +export const changeCheckboxState = (evt, question) => { + return { + type: CHANGE_CHECKBOX_STATE, + evt, + question + }; +}; + +export const deleteSelectedQuestion = (question, checkBoxes) => { + return { + type: DELETE_SELECTED_QUESTION, + question, + checkBoxes + }; +}; + +export function getQuestions(lessonId) { + return (dispatch) => { + questions(lessonId).then(data => { + dispatch({ type: GET_QUESTION, questions: data, lessonId }); + }); + }; +} + +export function getSpecificQuestion(lessonId, questionId) { + return (dispatch) => { + question(lessonId, questionId).then(data => { + dispatch({ type: GET_SPECIFIC_QUESTION, data }); + }); + }; +} + +export function refreshTheReducer(newQuestion) { + return { + type: REFRESH_THE_REDUCER, + newQuestion + }; +} + +export function sendQuestion(question) { + return { + type: QUESTION_IN_EDITING, + question + }; +} +export function sendErrorAction(error) { + return { + type: ERROR, + error + }; +} +export function addOneQuestion(question) { + return { + type: ADD_ONE_QUESTION, + question + }; +} \ No newline at end of file diff --git a/programmero-frontend/src/actions/userActions.js b/programmero-frontend/src/actions/userActions.js new file mode 100644 index 0000000..d4d73a4 --- /dev/null +++ b/programmero-frontend/src/actions/userActions.js @@ -0,0 +1,35 @@ +import { SET_USER, CREATE_USER, FILL_USER_PASSWORD, RESET_USER_REDUCER } from "./actionTypes"; +import { createUser, fillUserPassword } from "../data/userData"; + +export const setUser = (isAdmin, email, name) => { + return { + type: SET_USER, + isAdmin, + name, + email + }; +}; + +export const createUserAction = (email, name, isAdmin) => { + return (dispatch) => { + return createUser(email, name, isAdmin).then(statusCode => { + dispatch({ type: CREATE_USER, statusCode: statusCode }); + }).then(() => { + return Promise.resolve(); + }); + }; +}; + +export const fillUserPasswordAction = (pass, passrepeat, mailToken) => { + return (dispatch) => { + return fillUserPassword(pass, passrepeat, mailToken).then(statusCode => { + dispatch({ type: FILL_USER_PASSWORD, statusCode: statusCode }); + }).then(() => { + return Promise.resolve(); + }); + }; +}; + +export const resetUserReducer = () => { + return { type: RESET_USER_REDUCER }; +}; \ No newline at end of file diff --git a/programmero-frontend/src/api/giphy.js b/programmero-frontend/src/api/giphy.js new file mode 100644 index 0000000..d34e5f2 --- /dev/null +++ b/programmero-frontend/src/api/giphy.js @@ -0,0 +1,3 @@ +let giphyApi = "KeqsB6vg60HuAuigSn5722bpBut1Wy2Z"; + +export default giphyApi; \ No newline at end of file diff --git a/programmero-frontend/src/components/App.js b/programmero-frontend/src/components/App.js new file mode 100644 index 0000000..e8dcb80 --- /dev/null +++ b/programmero-frontend/src/components/App.js @@ -0,0 +1,63 @@ +import React, { Component } from "react"; +import AdminUI from "./pages/AdminUI"; +import {bindActionCreators} from "redux"; +import {connect} from "react-redux"; +import jwtDecode from "jwt-decode"; +import {setUser} from "../actions/userActions"; +import {BrowserRouter , Switch , Route} from "react-router-dom"; +import Addquestion from "./pages/AddQuestion"; +import AddPassword from "./pages/AddPassword"; +import NotFound from "./pages/NotFound"; +import Login from "./pages/Login"; +import QuestionManagement from "./pages/QuestionManagement"; +import LessonManagement from "./pages/LessonManagement"; +import QuestionEdit from "./pages/QuestionEdit"; + +import Invited from "./Inviting/Invited"; +import Student from "./student/Student"; +import InviteUser from "./pages/InviteUser"; + + + +class App extends Component { + constructor(props){ + super(props); + if(localStorage.getItem("usertoken") !== null){ + let userToken = localStorage.usertoken; + userToken = jwtDecode(userToken); + const {name,isAdmin,email} = userToken; + this.props.setUser(isAdmin,email,name); + } + } + render() { + return ( + + + + + + + + + + + + + + + + + ); + } +} +const mapStateToProps = state => { + return {}; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({setUser}, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(App); \ No newline at end of file diff --git a/programmero-frontend/src/components/Inviting/Invited.jsx b/programmero-frontend/src/components/Inviting/Invited.jsx new file mode 100644 index 0000000..6732d22 --- /dev/null +++ b/programmero-frontend/src/components/Inviting/Invited.jsx @@ -0,0 +1,122 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import isEmpty from "lodash/isEmpty"; + +class Invited extends Component { + constructor(props) { + super(props); + this.state = { + email: "example@example.nl", + name: "example", + password: "", + errors: { + notValidated: "yes" + } + }; + } + + render() { + const validatePassword = event => { + let { password } = this.refs; + let errors = {}; + if (event.target.name === "repeat") { + if (event.target.value !== password.value) { + errors.repeat = + "Herhaling wachtwoord komt niet heen met de wachtwoord"; + } + } else { + if (event.target.value.length < 8) { + errors.password = "Wachtwoord moet minimal 8 tekens bevaten!"; + } + } + this.setState({ errors }); + }; + const checkError = error => (error ? "is-invalid" : ""); + + const signUp = () => { + if (isEmpty(this.state.errors)) { + if ( + this.refs.password.value.length >= 8 && + this.refs.repeat.value.length >= 8 + ) { + this.props.history.push("/"); + } + } + }; + let { errors } = this.state; + return ( +
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
{errors.password}
+
+
+
+ +
+ +
{errors.repeat}
+
+ +
+
+
+
+ ); + } +} + +export default Invited; diff --git a/programmero-frontend/src/components/pages/AddPassword.jsx b/programmero-frontend/src/components/pages/AddPassword.jsx new file mode 100644 index 0000000..72a513e --- /dev/null +++ b/programmero-frontend/src/components/pages/AddPassword.jsx @@ -0,0 +1,149 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import { FormGroup, Label, Container } from "reactstrap"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { + fillUserPasswordAction, + resetUserReducer +} from "../../actions/userActions"; +import { checkTheMailToken } from "../../data/userData"; + +export class AddPassword extends Component { + constructor(props) { + super(props); + this.state = { + shwoForm: false, + notification: {}, + count: 3 + }; + checkTheMailToken(this.props.match.params.mailToken) + .then(res => { + this.setState({ shwoForm: res }); + }) + .catch(); + } + tick() { + this.setState({ count: (this.state.count + 1) }); + } + componentWillUnmount() { + clearInterval(this.timer); + } + handleAlert(success, fail) { + if (fail) { + this.props.resetUserReducer(); + this.setState({ + notification: { type: "warning", message: "User bestaat al" } + }); + } else if (success) { + this.props.resetUserReducer(); + this.setState({ + notification: { + type: "success", + message: "Wachtwoord is gezet, u kunt nu inloggen!. U wordt verwezen naar inlog pagina" + } + }); + setInterval(() => this.props.history.push("/"), 3000); + } + } + sendPasswords() { + const pass = document.getElementById("pass").value; + const passrepeat = document.getElementById("passrepeat").value; + const mailToken = this.props.match.params.mailToken; + if (pass === "" || passrepeat === "") { + this.setState({ + notification: { + type: "warning", + message: "Wachtwoord and wachtwoord herhaling mogen niet leeg zijn" + } + }); + } else { + if (pass === passrepeat) { + this.props + .fillUserPasswordAction(pass, passrepeat, mailToken) + .then(() => { + this.handleAlert(this.props.success, this.props.fail); + }); + } else { + this.setState({ + notification: { + type: "warning", + message: "Wachtwoord and wachtwoord herhaling moeten gelijk zijn" + } + }); + } + } + } + + render() { + return ( +
+
+ +
+ {this.state.notification ? ( +
+ {this.state.notification.message} +
+ ) : ( + "" + )} +
+

Wachtwoord invoeren

+ {this.state.shwoForm ? ( +
+ + + this.setState({ notification: {} })} + /> +
+ + this.setState({ notification: {} })} + /> +
+ +
+ ) : ( +
+ De mail token is niet bij ons bekend +
+ )} +
+
+ ); + } +} + +const mapStateToProps = state => { + return { + success: state.userReducer.passwordChangedSuccessfull, + fail: state.userReducer.passwordChangedUnSuccessfull + }; +}; + +const matchDispatchToProps = dispatch => { + return bindActionCreators( + { fillUserPasswordAction, resetUserReducer }, + dispatch + ); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(AddPassword); diff --git a/programmero-frontend/src/components/pages/AddQuestion.jsx b/programmero-frontend/src/components/pages/AddQuestion.jsx new file mode 100644 index 0000000..54198f6 --- /dev/null +++ b/programmero-frontend/src/components/pages/AddQuestion.jsx @@ -0,0 +1,251 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import enter from "../../images/keyboard-key-enter.png"; +import { + FormGroup, + Label, + InputGroupAddon, + InputGroup, + Button, + ListGroup, + UncontrolledCollapse, + Card + +} from "reactstrap"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import { + addQuestion, + addAnswer, + inEditing, + deleteAnswer +} from "../../actions/questionManagementAction"; +import { refreshTheReducer } from "../../actions/questionManagementAction"; +import { postQuestion } from "../../data/questionData"; +import { getLessonsAction } from "../../actions/lessonManagementAction"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faTrash, faCheck } from "@fortawesome/free-solid-svg-icons"; +import { Link } from "react-router-dom"; + +import { getQuestions } from "../../actions/questionManagementAction"; +library.add(faPlus, faTrash, faCheck); + +class AddQuestion extends Component { + constructor(props) { + super(props); + + const { isAdmin } = this.props; + if (!isAdmin) { + this.props.history.push("/student"); + } + this.props.getLessonsAction(); + this.state = { + showSave: false, + isLoading: false, + successed: false, + indexClicked: -1 + }; + this.answer = React.createRef(); + this.question = React.createRef(); + } + render() { + const handleQuestion = event => { + if (event.target.value.length > 5) + return this.setState({ showSave: true }); + this.setState({ showSave: false }); + }; + const sendQuestion = () => { + this.setState({ isLoading: true }); + let question = this.question.current.value; + let { answers } = this.props; + let lessonId = this.refs.lesson.value; + postQuestion(lessonId, question, answers) + .then(res => { + this.setState({ successed: true, isLoading: false }); + this.props.refreshTheReducer(); + if (this.state.questionListId === lessonId) { + this.props.getQuestions(lessonId); + } + this.question.current.value = ""; + } + ) + .catch(this.setState({ isLoading: false })); + }; + const sendAnswer = () => { + let answer = this.answer.current.value; + if (answer.length > 0) { + this.props.addAnswer(answer, this.props.inEditingId); + this.answer.current.value = ""; + } + }; + const handleEdititing = (id, answerText) => { + this.answer.current.value = answerText; + this.props.inEditing(id); + }; + const handleDelete = id => { + this.answer.current.value = ""; + this.props.deleteAnswer(id); + }; + + const makeLessons = (index, program) => { + if (this.props.questions) { + return ( + {this.props.questions.map((codeCard) => { + return
{codeCard.question}
; + }) + } +
); + } + }; + + const handleEvent = (evt, indexClicked) => { + this.props.getQuestions(evt.target.value); + this.setState({ indexClicked: indexClicked, questionListId: evt.target.value }); + }; + const handleTheEnter = (event) => { + if (event.keyCode === 13) sendAnswer(); + }; + return ( +
+
+
+
+

Voeg vraag toe

+ { + this.state.successed ? +
+ De vraag is succesvol aangepast! +
: "" + } + + + + + + + + + + + + + + {this.props.inEditingId === "" ? ( + + ) : ( + + )} +
enter
+
+
+ +
+ +
+

Antwoord

+ +
    + {this.props.answers.map((answer, index) => { + if (index === this.props.inEditingId) { + return ( +
  • + {answer}  + +
  • + ); + } + return ( +
  • handleEdititing(index, answer)} + key={index} + className="answer fadeIn" + > + {answer} +
  • + ); + })} +
+
+
+ {this.state.showSave ? ( + + ) : ("")} +
+
+

Lesprogramma's

+
+
+ + {this.props.lessons.map((lesson, index) => { + return
+ {this.state.indexClicked === index ? + + : + } + + {this.state.indexClicked === index ? (makeLessons(index, lesson)) : ""} +
; + })} +
+
+ +
+
+
+ ); + } +} +const mapStateToProps = state => { + return { + answers: state.appReducer.answers, + inEditingId: state.appReducer.inEditing, + isAdmin: state.authReducer.isAdmin, + name: state.authReducer.name, + lessons: state.lessonManagementReducer.lessons, + questions: state.questionListReducer.questions + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators( + { addQuestion, addAnswer, getLessonsAction, inEditing, refreshTheReducer, deleteAnswer, getQuestions }, + dispatch + ); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(AddQuestion); diff --git a/programmero-frontend/src/components/pages/AdminUI.jsx b/programmero-frontend/src/components/pages/AdminUI.jsx new file mode 100644 index 0000000..375795a --- /dev/null +++ b/programmero-frontend/src/components/pages/AdminUI.jsx @@ -0,0 +1,102 @@ +import React, { Component } from "react"; +import { Container, Col, Row, Jumbotron } from "reactstrap"; +import { Link } from "react-router-dom"; +import Header from "../parts/Header"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { setUser } from "../../actions/userActions"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faPlus, + faWrench, + faGraduationCap, + faInfo, + faUser +} from "@fortawesome/free-solid-svg-icons"; + +library.add(faPlus, faWrench, faGraduationCap, faInfo, faUser); + + +class AdminUI extends Component { + constructor(props) { + super(props); + if (localStorage.getItem("usertoken") !== null) { + const { isAdmin } = this.props; + if (!isAdmin) this.props.history.push("/student"); + } else { + this.props.history.push("/"); + } + } + render() { + return
+
+ + + + + +
+ +
+

Voeg een vraag toe

+
+ + + + + + + +
+ +
+ +

Beheer vragen

+
+ + +
+ + + + + + +
+ +
+ +

Lesprogramma beheer

+
+ + + + + + +
+ +
+ +

Gebruiker toevoegen

+
+ + +
+
+
; + } +} + +const mapStateToProps = state => { + return { isAdmin: state.authReducer.isAdmin }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({ setUser }, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(AdminUI); \ No newline at end of file diff --git a/programmero-frontend/src/components/pages/InviteUser.jsx b/programmero-frontend/src/components/pages/InviteUser.jsx new file mode 100644 index 0000000..f8a94f0 --- /dev/null +++ b/programmero-frontend/src/components/pages/InviteUser.jsx @@ -0,0 +1,203 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import { Input, CustomInput, Button } from "reactstrap"; +import { Link } from "react-router-dom"; +import { createUserAction, resetUserReducer } from "../../actions/userActions"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export class InviteUser extends Component { + constructor(props) { + super(props); + const { isAdmin } = this.props; + if (!isAdmin) { + this.props.history.push("/student"); + } + this.state = { + emailInput: { value: "", errormsg: "" }, + nameInput: { value: "", errormsg: "" }, + isAdmin: false, + notification: {} + }; + } + + checkEmailInput(evt) { + let value = evt.target.value; + let emailInput = {}; + emailInput.value = value; + if (value.length === 0) { + emailInput.errormsg = "Dit invoerveld is leeg"; + } else if (value.length < 5) { + emailInput.errormsg = "Email moet minstens 5 karakters bevatten"; + } + this.setState({ emailInput: emailInput, notification: {} }); + } + + checkNameInput(evt) { + let value = evt.target.value; + let nameInput = {}; + nameInput.value = value; + if (value.length > 20) { + nameInput.errormsg = "Naam mag niet langer dan 20 karakters zijn"; + } else if (value.length === 0) { + nameInput.errormsg = "Dit invoerveld is leeg"; + } + this.setState({ nameInput: nameInput, notification: {} }); + } + + showErrorMsg(input) { + let emailErrorMsg = this.state.emailInput.errormsg; + let nameErrorMsg = this.state.nameInput.errormsg; + if (emailErrorMsg !== "" && input === "email") { + return {emailErrorMsg}; + } else if (nameErrorMsg !== "" && input === "name") { + return {nameErrorMsg}; + } + } + + changeCheckbox() { + this.setState({ isAdmin: !this.state.isAdmin }); + } + + showButton() { + if ( + this.state.emailInput.errormsg === undefined && + this.state.nameInput.errormsg === undefined + ) + return ( + + ); + else + return ( + + ); + } + handleResponse(success, fail) { + let notification = {}; + if (fail) { + notification = { + type: "danger", + message: + "Kan user niet uitnodigen, user is al uitgenodigd of is al actief" + }; + this.props.resetUserReducer(); + } else if (success) { + notification = { + type: "success", + message: "Uitnodiging verzonden" + }; + this.props.resetUserReducer(); + } + this.setState({ + notification + }); + } + + sendForm(evt) { + evt.preventDefault(); + this.props + .createUserAction( + this.state.emailInput.value, + this.state.nameInput.value, + this.state.isAdmin + ) + .then(() => { + this.handleResponse(this.props.success, this.props.fail); + }); + } + + render() { + var { path } = this.props.match; + path = path.replace("-", " "); + return ( +
+
+
+ +
+ {this.state.notification ?
{this.state.notification.message}
: ""} +
+
+

Gebruiker toevoegen

+
+
this.sendForm(evt)}> +
+ + this.checkEmailInput(evt)} + type="email" + name="Email" + /> + {this.showErrorMsg("email")} +
+
+ + this.checkNameInput(evt)} + type="text" + /> + {this.showErrorMsg("name")} +
+
+
+ this.changeCheckbox()} + name="CustomCheckbox" + label="Is beheerder?" + /> +
+
+ +
{this.showButton()}
+
+
+ + + +
+
+
+ ); + } +} + +const mapStateToProps = state => { + return { + success: state.userReducer.userSubmittedSuccesfull, + fail: state.userReducer.userSubmittedUnSuccesfull, + isAdmin: state.authReducer.isAdmin + }; +}; + +const matchDispatchToProps = dispatch => { + return bindActionCreators({ createUserAction, resetUserReducer }, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(InviteUser); diff --git a/programmero-frontend/src/components/pages/LessonManagement.jsx b/programmero-frontend/src/components/pages/LessonManagement.jsx new file mode 100644 index 0000000..e1e6de3 --- /dev/null +++ b/programmero-frontend/src/components/pages/LessonManagement.jsx @@ -0,0 +1,190 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import { Link } from "react-router-dom"; +import { + Container, + Col, + Row, + Button, + InputGroup, + Input, + ListGroupItem, + ListGroup, + ModalHeader, + Modal, + ModalBody, + ModalFooter +} from "reactstrap"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEdit, faTrashAlt, faChevronCircleLeft } from "@fortawesome/free-solid-svg-icons"; +import { createLessonAction, switchLessonSelection, deleteSelectedLessons, changeSelectedLessonName, getLessonsAction, resetStatusCodeAction } from "../../actions/lessonManagementAction"; + +library.add(faEdit, faTrashAlt, faChevronCircleLeft); + +export class LessonManagement extends Component { + + constructor(props) { + super(props); + this.props.getLessonsAction(); + this.state = { + showWarningPopup: false + }; + } + + componentDidUpdate() { + if (this.props.statusCode > 0) { + this.props.getLessonsAction(); + this.props.resetStatusCodeAction(this.props.statusCode); + } + } + + createLesson() { + const input = document.getElementById("lessonName"); + + const lesson = this.props.lessons.find(lesson => lesson.name === input.value); + + if (input.value.length > 3 && lesson === undefined) { + this.props.createLessonAction(input.value); + input.value = ""; + } + } + + switchLessonSelection(evt) { + this.props.switchLessonSelection(evt.target.value); + } + + deleteSelectedLessons() { + this.props.active.forEach(activeLessonName => { + let lesson = this.props.lessons.find(lesson => { + return lesson.name === activeLessonName; + }); + this.props.deleteSelectedLessons(lesson._id); + }); + + + this.setshowWarningPopup(false); + } + setshowWarningPopup(bool) { + this.setState({ showWarningPopup: bool }); + } + + changeSelectedLessonName() { + const checkboxes = document.getElementsByClassName("lessonCheckbox"); + const oldName = this.props.active[0]; + let newName = document.getElementById("newName").value; + + if (oldName !== newName && newName.length > 3) { + Array.prototype.forEach.call(checkboxes, checkbox => { + checkbox.checked = false; + }); + this.props.changeSelectedLessonName(this.props.lessons, oldName, newName); + newName = ""; + } + } + + render() { + const numberOfSelected = this.props.active.length; + const { error } = this.props; + return ( +
+
+

Lesprogramma beheer

+ + + + {error ?
{error}
: ""} +

Lesprogramma toevoegen

+
+ + + +
+ +
+ +
+ + + +

Lesprogrammas

+ +
+ + {this.props.lessons.map(lesson => +
+ this.switchLessonSelection(evt)} value={lesson.name} className="custom-control-input lessonCheckbox" id={`customCheck${lesson._id}`} /> + +
+
)} + +
+
+
+ {(numberOfSelected === 1) ? + : + + } + + {numberOfSelected === 1 ? + : + + } + + {(numberOfSelected > 0) ? + : + + } +
+ this.setshowWarningPopup(true)} className={this.props.className}> + this.setshowWarningPopup(false)}>Waarschuwing + + U staat op het punt de volgende lesprogramma's te verwijderen: + + {this.props.active.map(active => { + return {active}; + })} + + + + + + + + +
+ +
+
+ ); + } +} +const mapStateToProps = state => { + return { + lessons: state.lessonManagementReducer.lessons, + active: state.lessonManagementReducer.active, + statusCode: state.lessonManagementReducer.statusCode, + error: state.appReducer.error + }; +}; + +const matchDispatchToProps = dispatch => { + return bindActionCreators( + { + createLessonAction, + switchLessonSelection, + deleteSelectedLessons, + changeSelectedLessonName, + getLessonsAction, + resetStatusCodeAction + }, + dispatch + ); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(LessonManagement); diff --git a/programmero-frontend/src/components/pages/Login.jsx b/programmero-frontend/src/components/pages/Login.jsx new file mode 100644 index 0000000..5555857 --- /dev/null +++ b/programmero-frontend/src/components/pages/Login.jsx @@ -0,0 +1,140 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import { bindActionCreators } from "redux"; +import { setUser } from "../../actions/userActions"; +import jwtDecode from "jwt-decode"; +import { connect } from "react-redux"; +import { Form } from "reactstrap"; +import { validateInput } from "../../functions/validationFunctions"; +import { login } from "../../functions/authenticationFunctions"; + +export class Login extends Component { + constructor(props) { + super(props); + if (this.props.isLoggedIn) { + const sendingTo = this.props.isAdmin ? "/admin" : "/student"; + this.props.history.push(sendingTo); + } + this.state = { + email: "", + errors: {}, + isLoading: false + }; + } + render() { + let alert; + + if (this.state.notification) { + alert = ; + } + + const isValid = () => { + const { errors, isValid } = validateInput(this.state); + if (!isValid) { + return this.setState({ errors }); + } + return isValid; + }; + const onsubmit = event => { + event.preventDefault(); + if (isValid()) { + this.setState({ isLoading: true, notification: false }); + const user = { + email: this.state.email, + password: this.state.password + }; + login(user) + .then(res => { + this.setState({ errors: "", isLoading: false }); + localStorage.setItem("usertoken", res); + let userToken = res; + userToken = jwtDecode(userToken); + const { name, isAdmin, email } = userToken; + this.props.setUser(isAdmin, email, name); + (isAdmin) ? this.props.history.push("/admin") : this.props.history.push("/student"); + }) + .catch(err => { + this.setState({ + isLoading: false, + notification: true, + errors: { + [err.message]: `${err.message} niet goed` + } + }); + }); + } + }; + const onchange = event => { + this.setState({ [event.target.name]: event.target.value }); + }; + const { errors, isLoading } = this.state; + const checkError = error => (error ? "is-invalid" : ""); + return ( +
+
+
+
+ {alert} +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ ); + } +} +const mapStateToProps = state => { + return { + isAdmin: state.authReducer.isAdmin, + name: state.authReducer.name, + isLoggedIn: state.authReducer.isLoggedIn + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({ setUser }, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(Login); \ No newline at end of file diff --git a/programmero-frontend/src/components/pages/NotFound.jsx b/programmero-frontend/src/components/pages/NotFound.jsx new file mode 100644 index 0000000..eed367b --- /dev/null +++ b/programmero-frontend/src/components/pages/NotFound.jsx @@ -0,0 +1,16 @@ +import React from "react"; +class NotFound extends React.Component { + render() { + return ( +
+
+

404

+

Pagina niet gevonden

+ Ga naar Programmero +
+
+ ); + } +} + +export default NotFound; diff --git a/programmero-frontend/src/components/pages/QuestionEdit.jsx b/programmero-frontend/src/components/pages/QuestionEdit.jsx new file mode 100644 index 0000000..7dd7102 --- /dev/null +++ b/programmero-frontend/src/components/pages/QuestionEdit.jsx @@ -0,0 +1,293 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import enter from "../../images/keyboard-key-enter.png"; + +import { + FormGroup, + Label, + InputGroupAddon, + InputGroup, + Button, + UncontrolledCollapse, + ListGroup, + Card +} from "reactstrap"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import { + addQuestion, + addAnswer, + inEditing, + deleteAnswer, + refreshTheReducer, + sendErrorAction, + getSpecificQuestion, + getQuestions +} from "../../actions/questionManagementAction"; + +import { putQuestion } from "../../data/questionData"; +import { getLessonsAction } from "../../actions/lessonManagementAction"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faTrash, faCheck } from "@fortawesome/free-solid-svg-icons"; +import { Link } from "react-router-dom"; + +library.add(faPlus, faTrash, faCheck); + +class AddQuestion extends Component { + constructor(props) { + super(props); + this.props.getLessonsAction(); + this.state = { + showSave: true, + isLoading: false, + successed: false, + failed: false, + indexClicked: -1, + message: "" + }; + this.answer = React.createRef(); + this.question = React.createRef(); + this.counter = 0; + + this.props.getSpecificQuestion(this.props.match.params.lessonId, this.props.match.params.id); + } + + componentWillUpdate() { + this.counter++; + } + + render() { + const handleQuestion = event => { + if (event.target.value.length > 5) + return this.setState({ showSave: true }); + this.setState({ showSave: false }); + }; + const sendQuestion = () => { + this.setState({ isLoading: true }); + let question = this.question.current.value; + let { answers } = this.props; + putQuestion( + this.props.match.params.lessonId, + this.props.match.params.id, + question, + answers + ) + .then(res => { + this.setState({ successed: true, message: "Vraag succesvol veranderd!", isLoading: false }); + if (this.state.questionListId === this.props.match.params.lessonId) { + this.props.getQuestions(this.props.match.params.lessonId); + } + window.scrollTo(0, 0); + }) + .catch(error => { + this.props.sendErrorAction("iets ging mis. Probeer het later eens!"); + this.setState({ isLoading: false }); + }); + }; + const sendAnswer = () => { + let answer = this.answer.current.value; + if (answer.length > 0) { + this.props.addAnswer(answer, this.props.inEditingId); + this.answer.current.value = ""; + } + }; + const handleEdititing = (id, answerText) => { + this.answer.current.value = answerText; + this.props.inEditing(id); + }; + const handleDelete = id => { + this.answer.current.value = ""; + this.props.deleteAnswer(id); + }; + + const makeLessons = (index, program) => { + if (this.props.questions) { + return ( + {this.props.questions.map((codeCard) => { + return ; + }) + } + ); + } + }; + + const handleEvent = (evt, indexClicked) => { + this.setState({ indexClicked: indexClicked, questionListId: evt.target.value }); + this.props.getQuestions(evt.target.value); + }; + const handleTheEnter = (event) => { + if (event.keyCode === 13) sendAnswer(); + }; + const { error } = this.props; + + return ( +
+
+
+
+ {this.state.successed ? ( +
+ Prima! De vraag is aangepast! +
+ ) : ( + "" + )} + {error ? ( +
+ {error} +
+ ) : ( + "" + )} +

Vraag bewerken

+ + + + + + + + + + + {this.props.inEditingId === "" ? ( + + ) : ( + + )} +
Enter button
+
+
+
+
+

Antwoord

+ +
    + {this.props.answers.map((answer, index) => { + if (index === this.props.inEditingId) { + return ( +
  • + {answer}  + +
  • + ); + } + return ( +
  • handleEdititing(index, answer)} + key={index} + className="answer fadeIn" + > + {answer} +
  • + ); + })} +
+
+
+ {this.state.showSave ? ( + + ) : ( + "" + )} +
+
+

Lesprogramma's

+
+
+ + {this.props.lessons.map((lesson, index) => { + return ( +
+ {this.state.indexClicked === index ? ( + + ) : ( + + )} + + {this.state.indexClicked === index + ? makeLessons(index, lesson) + : ""} +
+ ); + })} +
+
+ +
+
+
+ ); + } +} +const mapStateToProps = state => { + return { + answers: state.appReducer.answers, + inEditingId: state.appReducer.inEditing, + lessons: state.lessonManagementReducer.lessons, + questions: state.questionListReducer.questions, + error: state.appReducer.error, + question: state.appReducer.question + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators( + { + addQuestion, + getSpecificQuestion, + addAnswer, + getLessonsAction, + inEditing, + refreshTheReducer, + deleteAnswer, + getQuestions, + sendErrorAction + }, + dispatch + ); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(AddQuestion); diff --git a/programmero-frontend/src/components/pages/QuestionManagement.jsx b/programmero-frontend/src/components/pages/QuestionManagement.jsx new file mode 100644 index 0000000..3da51a6 --- /dev/null +++ b/programmero-frontend/src/components/pages/QuestionManagement.jsx @@ -0,0 +1,150 @@ +import React, { Component } from "react"; +import Header from "../parts/Header"; +import { Table, Container, Row, Col, CustomInput, Button, ModalHeader, Modal, ModalBody, ModalFooter, ListGroup, ListGroupItem } from "reactstrap"; +import { Link } from "react-router-dom"; +import { bindActionCreators } from "redux"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faPencilAlt, faGraduationCap, faInfo, faChevronCircleLeft } from "@fortawesome/free-solid-svg-icons"; +import { selectAllQuestions, setCheckboxesState, changeCheckboxState, deleteSelectedQuestion, getQuestions, sendQuestion } from "../../actions/questionManagementAction"; +import { getLessonsAction } from "../../actions/lessonManagementAction"; +import { connect } from "react-redux"; +library.add(faPlus, faPencilAlt, faGraduationCap, faInfo, faChevronCircleLeft); + +export class QuestionManagement extends Component { + /** + * Fetches the lessons from the server. + * @param {*} props + */ + constructor(props) { + super(props); + this.props.getLessonsAction(); + this.state = { + showWarningPopup: false, + questionstoDelete: [] + }; + } + + /** + * Fetches the lessons from the server. + */ + didComponentRender() { + this.props.getLessonsAction(); + } + + getCodeCardsOfLessons() { + let selectValue = document.getElementById("lessonSelector").value; + if (selectValue.length > 0) { + this.props.getQuestions(selectValue); + } + } + setshowWarningPopup(bool) { + let questionstoDelete = this.props.questions.filter(lesson => { + return lesson.checked === true; + }); + this.setState({ showWarningPopup: bool, questionstoDelete: questionstoDelete }); + } + + deleteSelectedQuestionHandler() { + this.props.deleteSelectedQuestion(this.props.lessons, document.getElementsByName("checkbox")); + this.setshowWarningPopup(false); + } + + render() { + let i = -1; + return ( +
+
+ +
+ +

Vragen beheren

+

Selecteer een lesprogramma

+ +
+
+
+ +
+ + + + + + + + + + { + (this.props.questions.length === 0) ? + + : + this.props.questions.map(question => { + return + + + + ; + }) + } + +
SelecteerVraagWijzig
Er zijn nog geen vragen aangemaakt voor deze les!
this.props.changeCheckboxState(evt, this.props.questions)} id={question._id} inline />{question.question} + { this.props.sendQuestion(question); }} to={`/question/${question._id}/${this.props.lessonId}`}> + + +
+
+ + + + + + + + +
+ this.setshowWarningPopup(true)} className={this.props.className}> + this.setshowWarningPopup(false)}>Waarschuwing + + U staat op het punt de volgende vragen te verwijderen: + + {this.state.questionstoDelete.map(active => { + return {active.question}; + })} + + + + + + + +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + questions: state.questionListReducer.questions, + lessons: state.lessonManagementReducer.lessons, + lessonId: state.questionListReducer.lessonId + + }; +} + +const matchDispatchToProps = dispatch => { + return bindActionCreators({ sendQuestion, getQuestions, getLessonsAction, deleteSelectedQuestion, changeCheckboxState, setCheckboxesState, selectAllQuestions }, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(QuestionManagement); \ No newline at end of file diff --git a/programmero-frontend/src/components/pages/UserUI.jsx b/programmero-frontend/src/components/pages/UserUI.jsx new file mode 100644 index 0000000..28cda94 --- /dev/null +++ b/programmero-frontend/src/components/pages/UserUI.jsx @@ -0,0 +1,25 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import UserHeader from "../parts/UserHeader"; + + +class UserUI extends Component { + render() { + return
+ +
; + } +} + +const mapStateToProps = state => { + return {}; +}; + +const matchDispatchToProps = dispatch => ({ + +}); + +export default connect( + mapStateToProps, + matchDispatchToProps +)(UserUI); diff --git a/programmero-frontend/src/components/parts/Header.jsx b/programmero-frontend/src/components/parts/Header.jsx new file mode 100644 index 0000000..73ddbe1 --- /dev/null +++ b/programmero-frontend/src/components/parts/Header.jsx @@ -0,0 +1,105 @@ +import React, { Component } from "react"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import logo from "../../images/logo-programmero.svg"; +import { + Collapse, + Navbar, + NavLink, + NavbarToggler, + Nav, + NavItem, + UncontrolledDropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + NavbarBrand +} from "reactstrap"; +import { Link } from "react-router-dom"; + +class Header extends Component { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.state = { + isOpen: false + }; + } + + toggle() { + this.setState({ + isOpen: !this.state.isOpen + }); + } + + logout(event) { + event.preventDefault(); + localStorage.clear(); + window.location.replace("/"); + } + + render() { + const { isAdmin, name, isLoggedIn } = this.props; + return ( +
+ + + + + + + { + isLoggedIn ? + : "" + } + + +
+ ); + } +} +const mapStateToProps = state => { + return { + isAdmin: state.authReducer.isAdmin, + name: state.authReducer.name, + isLoggedIn: state.authReducer.isLoggedIn + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({}, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(Header); \ No newline at end of file diff --git a/programmero-frontend/src/components/parts/OefenProgrammaHeader.jsx b/programmero-frontend/src/components/parts/OefenProgrammaHeader.jsx new file mode 100644 index 0000000..fe92615 --- /dev/null +++ b/programmero-frontend/src/components/parts/OefenProgrammaHeader.jsx @@ -0,0 +1,77 @@ +import React, { Component } from "react"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { + Collapse, + Navbar, + NavbarToggler, + Nav, + NavItem, + NavbarBrand +} from "reactstrap"; + + +export class OefenProgrammaHeader extends Component { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.state = { + isOpen: false + }; + if (this.props.match !== undefined && !this.props.match.params.id) { + this.props.getLessonprogramAction(this.props.match.params.id); + } + } + + toggle() { + this.setState({ + isOpen: !this.state.isOpen + }); + } + + logout(event) { + event.preventDefault(); + localStorage.clear(); + window.location.replace("/"); + } + + render() { + const { numberOfQuestion, allQuestion, lesson, lessonScore } = this.props; + return ( +
+ + +

Vraag {numberOfQuestion} / {allQuestion} {lesson.name}

+
+ + + + +
+
+ ); + } +} +const mapStateToProps = state => { + return { + isAdmin: state.authReducer.isAdmin, + name: state.authReducer.name, + isLoggedIn: state.authReducer.isLoggedIn, + score: state.studentReducer.score, + numberOfQuestion: state.studentReducer.numberOfQuestion, + allQuestion: state.studentReducer.allQuestion, + lessonprogram: state.studentReducer.lessonProgram + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({}, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(OefenProgrammaHeader); \ No newline at end of file diff --git a/programmero-frontend/src/components/parts/UserHeader.jsx b/programmero-frontend/src/components/parts/UserHeader.jsx new file mode 100644 index 0000000..0eaeb55 --- /dev/null +++ b/programmero-frontend/src/components/parts/UserHeader.jsx @@ -0,0 +1,79 @@ +import React, { Component } from "react"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { + Collapse, + Navbar, + NavLink, + NavbarToggler, + Nav, + NavItem, + NavbarBrand +} from "reactstrap"; + +export class UserHeader extends Component { + constructor(props) { + super(props); + this.toggle = this.toggle.bind(this); + this.state = { + isOpen: false + }; + + } + + toggle() { + this.setState({ + isOpen: !this.state.isOpen + }); + } + + logout(event) { + event.preventDefault(); + localStorage.clear(); + window.location.replace("/"); + } + + render() { + const { name, isLoggedIn, score } = this.props; + return ( +
+ + +

{name} - score {score}

+
+ + + { + isLoggedIn ? + : "" + } + +
+
+ ); + } +} +const mapStateToProps = state => { + return { + isAdmin: state.authReducer.isAdmin, + name: state.authReducer.name, + isLoggedIn: state.authReducer.isLoggedIn, + score: state.studentReducer.score + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({}, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(UserHeader); \ No newline at end of file diff --git a/programmero-frontend/src/components/student/Feedback.jsx b/programmero-frontend/src/components/student/Feedback.jsx new file mode 100644 index 0000000..c9b70c2 --- /dev/null +++ b/programmero-frontend/src/components/student/Feedback.jsx @@ -0,0 +1,170 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Link } from "react-router-dom"; +import OefenProgrammaHeader from "../parts/OefenProgrammaHeader"; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"; +import { sendErrorAction } from "../../actions/questionManagementAction"; +import { bindActionCreators } from "redux"; +import { + getCodeCardAction, + getTheAnswers, + getScoreAction, + getLessonprogramScoreAction, + getLessonAction +} from "../../actions/practiceAction"; + +export class Feedback extends Component { + constructor(props) { + super(props); + this.props.getLessonAction(this.props.match.params.id); + this.props.getLessonprogramScoreAction(this.props.match.params.id); + this.props.getCodeCardAction(this.props.match.params.id); + this.props.getScoreAction(); + + this.state = { + modal: false + }; + this.toggle = this.toggle.bind(this); + } + toggle() { + this.setState({ + modal: !this.state.modal + }); + } + + render() { + + const compareTheAnswer = (correct, answer) => + correct === answer ? "success" : "false"; + const { + correctAnswer, + studentAnswer, + question, + correct, + addedScore, + lessonScore + } = this.props; + const nextQuestion = () => { + const link = this.props.lastQuestion + ? "/student/" + this.props.match.params.id + "/results" + : "/student/practice/" + this.props.match.params.id; + this.props.history.push(link); + }; + const { error } = this.props; + return ( +
+ + +
+ + {error ? ( +
+ {error} +
+ ) : ( + "" + )} + +

+{addedScore}

+

{question}

+
+

Jouw antwoord

+
    + {studentAnswer.map((answer, index) => ( +
  • + {answer} +
  • + ))} +
+
+
+

Correcte antwoord

+
    + {correctAnswer.map((answer, index) => ( +
  • + {answer} +
  • + ))} +
+
+
+ + +
+
+ + Oefening bëeindigen + Wil je zeker de Oefeing bëeindigen? + + {" "} + + Ja + + + +
+ ); + } +} + +const mapStateToProps = state => { + return { + correctAnswer: state.studentReducer.correctAnswer, + studentAnswer: state.studentReducer.studentAnswer, + question: state.studentReducer.question, + correct: state.studentReducer.correct, + lastQuestion: state.studentReducer.lastQuestion, + addedScore: state.studentReducer.addedScore, + lessonScore: state.studentReducer.lessonScore, + lesson: state.studentReducer.lesson, + numberOfQuestion: state.studentReducer.numberOfQuestion, + error: state.appReducer.error + }; +}; + +const matchDispatchToProps = dispatch => + bindActionCreators( + { getCodeCardAction, getTheAnswers, getScoreAction, sendErrorAction, getLessonprogramScoreAction, getLessonAction }, + dispatch + ); + +export default connect( + mapStateToProps, + matchDispatchToProps +)(Feedback); diff --git a/programmero-frontend/src/components/student/LessonprogramResult.jsx b/programmero-frontend/src/components/student/LessonprogramResult.jsx new file mode 100644 index 0000000..aee6842 --- /dev/null +++ b/programmero-frontend/src/components/student/LessonprogramResult.jsx @@ -0,0 +1,80 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import Header from "../parts/Header"; +import giphyApiKey from "../../api/giphy.js"; +import giphyRandom from "giphy-random"; +import "react-circular-progressbar/dist/styles.css"; +import { getEndLessonAction } from "../../actions/practiceAction"; + +export class LessonprogramResult extends Component { + constructor(props) { + super(props); + + this.state = { + gif: "", + loaded: false + }; + + this.props.getEndLessonAction(this.props.match.params.id); + + (async () => { + try { + const API_KEY = giphyApiKey; + + const { data } = await giphyRandom(API_KEY, { tag: "happy", rating: "g" }); + this.setState({ + gif: data.images.original.url, + loaded: true + }); + + } catch (err) { + this.setState({ + gif: "https://media3.giphy.com/media/l41YxNLbtPJohIdKU/giphy.gif", + loaded: true + }); + } + })(); + } + + render() { + return
+
+
+
+

{this.props.lessonName}

+
+
+

Nieuwe score: {this.props.lessonScore}

+
+
+

Hoeveelheid vragen goed: {this.props.amountCorrectQuestions}/{this.props.amountQuestions}

+
+
+ Succes gif +
+ +
+
; + } +} + +const mapStateToProps = state => { + return { + lessonScore: state.studentReducer.lessonScore, + lessonName: state.studentReducer.lessonProgram, + amountCorrectQuestions: state.studentReducer.amountCorrectQuestions, + amountQuestions: state.studentReducer.amountQuestions + }; +}; + +const matchDispatchToProps = dispatch => { + return bindActionCreators({ getEndLessonAction }, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(LessonprogramResult); diff --git a/programmero-frontend/src/components/student/Practice.jsx b/programmero-frontend/src/components/student/Practice.jsx new file mode 100644 index 0000000..e772b3b --- /dev/null +++ b/programmero-frontend/src/components/student/Practice.jsx @@ -0,0 +1,177 @@ +import React, { Component } from "react"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import { sendErrorAction } from "../../actions/questionManagementAction"; +import dragula from "react-dragula"; +import "dragula/dist/dragula.css"; +import { + getCodeCardAction, + getTheAnswers, + getScoreAction, + getLessonprogramScoreAction, + getLessonAction +} from "../../actions/practiceAction"; +import OefenProgrammaHeader from "../parts/OefenProgrammaHeader"; +import { Link } from "react-router-dom"; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"; +import { sendAnswerBackend } from "../../data/practice"; + +export class Practice extends Component { + constructor(props) { + super(props); + this.props.getLessonAction(this.props.match.params.id); + this.props.getLessonprogramScoreAction(this.props.match.params.id); + this.props.getCodeCardAction(this.props.match.params.id); + this.state = { + containers: [], + modal: false + }; + this.toggle = this.toggle.bind(this); + this.props.getScoreAction(); + } + toggle() { + this.setState({ + modal: !this.state.modal + }); + } + componentDidMount() { + dragula(this.state.containers, { revertOnSpill: true }); + } + render() { + const { question, answers, error, lessonScore } = this.props; + const codeParts = componentBackingInstance => { + if (componentBackingInstance) { + this.state.containers.push(componentBackingInstance); + } + }; + + const studentAnswer = componentBackingInstance => { + if (componentBackingInstance) { + this.state.containers.push(componentBackingInstance); + } + }; + const sendAnswer = link => { + let studentAnswers = Array.from( + document.querySelectorAll("#studentAnswers>li") + ); + const newAnswers = studentAnswers.map( + studentAnswer => studentAnswer.textContent + ); + if (answers.length === newAnswers.length) { + sendAnswerBackend(this.props.match.params.id, newAnswers, this.props.numberOfQuestion) + .then(res => { + this.props.getTheAnswers(res, newAnswers); + }) + .catch(error => { + this.props.sendErrorAction( + error.message?error.message:"Server heeft niet gereageerd " + ); + this.setState({ isLoading: false }); + }); + this.props.history.push( + "/student/" + this.props.match.params.id + "/feedback" + ); + } + }; + return ( +
+ +
+ + {error ? ( +
+ {error} +
+ ) : ( + "" + )} +

{question}

+
    +
      + {answers.map((answer, index) => ( +
    • + {answer} +
    • + ))} +
    +
    + + +
    +
    + + + Oefening bëeindigen + + Wil je zeker de Oefeing bëeindigen? + + {" "} + + Ja + + + +
    +
+
+ ); + } +} + +const mapStateToProps = state => { + return { + question: state.studentReducer.question, + answers: state.studentReducer.answerParts, + lessonScore: state.studentReducer.lessonScore, + lesson: state.studentReducer.lesson, + numberOfQuestion: state.studentReducer.numberOfQuestion, + error: state.appReducer.error + }; +}; +const matchDispatchToProps = dispatch => + bindActionCreators( + { getCodeCardAction, getTheAnswers, getScoreAction, sendErrorAction, getLessonprogramScoreAction, getLessonAction }, + dispatch + ); + +export default connect( + mapStateToProps, + matchDispatchToProps +)(Practice); diff --git a/programmero-frontend/src/components/student/Student.jsx b/programmero-frontend/src/components/student/Student.jsx new file mode 100644 index 0000000..d1745c5 --- /dev/null +++ b/programmero-frontend/src/components/student/Student.jsx @@ -0,0 +1,47 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Switch, Route } from "react-router-dom"; +import StudentProgramms from "./StudentProgramms"; +import Practice from "./Practice"; +import Feedback from "./Feedback"; +import { bindActionCreators } from "redux"; +import { getScoreAction } from "../../actions/practiceAction"; +import LessonprogramResult from "./LessonprogramResult"; + + +export class Student extends Component { + constructor(props) { + super(props); + if (!this.props.isLoggedIn) { + this.props.history.push("/"); + } + } + render() { + return ( +
+ + + + + + +
+ ); + } +} + +const mapStateToProps = state => { + return { + isLoggedIn: state.authReducer.isLoggedIn + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({ getScoreAction }, dispatch); +}; +export default connect( + mapStateToProps, + matchDispatchToProps, +)(Student); + + + diff --git a/programmero-frontend/src/components/student/StudentProgramms.jsx b/programmero-frontend/src/components/student/StudentProgramms.jsx new file mode 100644 index 0000000..ed7195b --- /dev/null +++ b/programmero-frontend/src/components/student/StudentProgramms.jsx @@ -0,0 +1,75 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import { getLessonsAction } from "../../actions/lessonManagementAction"; +import { sendErrorAction } from "../../actions/questionManagementAction"; +import UserHeader from "../parts/UserHeader"; +import { creatLessonResults } from "../../data/practice"; +import { getScoreAction } from "../../actions/practiceAction"; + +class StudentProgramms extends Component { + constructor(props) { + super(props); + this.props.getLessonsAction(); + this.props.getScoreAction(); + + } + render() { + + const onclick = lessonId => { + creatLessonResults(lessonId) + .then(res => this.props.history.push("/student/practice/" + lessonId)) + .catch(error => { + this.props.sendErrorAction(error.message?error.message:"Server heeft niet gereageerd "); + this.setState({ isLoading: false }); + }); + }; + const { error, score } = this.props; + return ( +
+ +
+

Lesprogramma's

+ {error ? ( +
+ {error} +
+ ) : ( + "" + )} +
+ {this.props.lessons.map(lesson => ( +
+
+

{lesson.name}

+ +
+
+ ))} +
+
+
+ ); + } +} + +const mapStateToProps = state => { + return { + lessons: state.lessonManagementReducer.lessons, + score: state.studentReducer.score, + error: state.appReducer.error + }; +}; +const matchDispatchToProps = dispatch => { + return bindActionCreators({ getLessonsAction, sendErrorAction, getScoreAction }, dispatch); +}; + +export default connect( + mapStateToProps, + matchDispatchToProps +)(StudentProgramms); diff --git a/programmero-frontend/src/data/lessonsData.js b/programmero-frontend/src/data/lessonsData.js new file mode 100644 index 0000000..cb1709e --- /dev/null +++ b/programmero-frontend/src/data/lessonsData.js @@ -0,0 +1,69 @@ + +/* +Async action creators +*/ + +export const getLessons = async () => { + const url = "http://localhost:4000/lessons/"; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status > 300) { + throw new Error(`HTTP Request went wrong: got "${response.statusText}" for "${url}"`); + } + const json = await response.json(); + return json; +}; + +export const createLesson = async (lesson) => { + const response = await fetch("http://localhost:4000/lessons/", { + method: "POST", + body: JSON.stringify(lesson), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + }); + if (response.status >300) { + throw new Error(`${response.status}: HTTP Request ging verkeerd, verkregen ${response.statusText}`); + } + return response; +}; + + + +export const updateLesson = async (lessonId, lesson ) => { + const response = await fetch("http://localhost:4000/lessons/"+ lessonId, { + method: "Put", + body: JSON.stringify(lesson), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + }); + if (response.status >300) { + throw new Error(`${response.status}: HTTP Request ging verkeerd, verkregen ${response.statusText}`); + } + return response.status; +}; + +export const deleteLesson = async (lessonId) => { + const response = await fetch("http://localhost:4000/lessons/" + lessonId, { + method: "DELETE", + credentials: "include", + mode: "cors", + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status >300) { + throw new Error(`${response.status}: HTTP Request ging verkeerd, verkregen ${response.statusText}`); + } + return response.status; +}; \ No newline at end of file diff --git a/programmero-frontend/src/data/practice.js b/programmero-frontend/src/data/practice.js new file mode 100644 index 0000000..f81b9fe --- /dev/null +++ b/programmero-frontend/src/data/practice.js @@ -0,0 +1,119 @@ +export const getCodeCard = async (lessonId) => { + const url = "http://localhost:4000/lessons/"+lessonId+"/codecards/student"; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status >300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}" ` + ); + } + const json = await response.json(); + return json; +}; + +export const creatLessonResults = async (lessonId) => { + const response = await fetch("http://localhost:4000/users/lessonresults/"+lessonId, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + }); + if (response.status > 300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}" ` + ); + } + const json = await response.json(); + return json; +}; + +export const sendAnswerBackend = async (lessonId,answer, currentQuestionIndex) => { + const response = await fetch("http://localhost:4000/users/lessonresults/"+lessonId+"/answer", { + method: "POST", + body: JSON.stringify({answer, index: currentQuestionIndex - 1 }), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + }); + if (response.status > 300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}"` + ); + } + const json = await response.json(); + return json; +}; + +export const getTheScore = async () => { + const url = "http://localhost:4000/users/score"; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status > 300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}""` + ); + } + const json = await response.json(); + return json; +}; + +export const getLesson = async (lessonId) => { + const url = "http://localhost:4000/lessons/"+lessonId; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status > 300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}"` + ); + } + const json = await response.json(); + return json; +}; + +export const getLessonprogramScore = async (lessonId) => { + const url = "http://localhost:4000/users/score/"+lessonId; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status > 300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}"` + ); + } + const json = await response.json(); + return json; +}; + +export const getEndLesson = async (lessonId) => { + const url = "http://localhost:4000/users/lessonresults/" + lessonId + "/end"; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + + if (response.status > 300) { + throw new Error( + `${response.status} : HTTP Request went wrong: got "${response.statusText}"` + ); + } + const json = await response.json(); + return json; +}; \ No newline at end of file diff --git a/programmero-frontend/src/data/questionData.js b/programmero-frontend/src/data/questionData.js new file mode 100644 index 0000000..307e6e1 --- /dev/null +++ b/programmero-frontend/src/data/questionData.js @@ -0,0 +1,95 @@ +export async function questions(lessonId) { + const url = "http://localhost:4000/lessons/" + lessonId + "/codecards"; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status !== 200) { + throw new Error( + `HTTP Request went wrong: got "${response.statusText}" for "${url}"` + ); + } + return await response.json(); +} + +export async function question(lessonId, questionId) { + const url = "http://localhost:4000/lessons/"+ lessonId +"/codecards/" + questionId; + const response = await fetch(url, { + headers: { + "Authorization": localStorage.getItem("usertoken") + } + }); + if (response.status !== 200) { + throw new Error( + `HTTP Request went wrong: got "${response.statusText}" for "${url}"` + ); + } + return await response.json(); +} + +export async function deleteQuestion(lessonId, questionId) { + let response = await fetch("http://localhost:4000/lessons/" + lessonId + "/codecards/" + questionId, { + method: "Delete", + body: JSON.stringify({}), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + }); + if (response.status !== 204) { + throw new Error( + `HTTP Request went wrong: got "${response.statusText}"` + ); + } + let status = await response.status; + return status; +} + +export async function postQuestion(lessonId, question, answer) { + const response = await fetch( + "http://localhost:4000/lessons/" + lessonId + "/codecards", + { + method: "POST", + body: JSON.stringify({ question, answer, number: 0 }), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + } + ); + if (response.status > 300) { + throw new Error( + `HTTP Request went wrong: got "${response.statusText}"` + ); + } + const json = await response.json(); + return json; +} + +export async function putQuestion(lessonId, codeCardId, question, answer) { + const response = await fetch( + "http://localhost:4000/lessons/" + lessonId + "/codecards/" + codeCardId, + { + method: "PUT", + body: JSON.stringify({ question, answer, number: 0 }), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + } + ); + if (response.status !== 200) { + throw new Error( + `HTTP Request went wrong: got "${response.statusText}"` + ); + } + const json = await response.json(); + return json; +} \ No newline at end of file diff --git a/programmero-frontend/src/data/userData.js b/programmero-frontend/src/data/userData.js new file mode 100644 index 0000000..7f25908 --- /dev/null +++ b/programmero-frontend/src/data/userData.js @@ -0,0 +1,46 @@ +export async function createUser(email, name, isAdmin) { + const response = await fetch("http://localhost:4000/users/", { + method: "POST", + body: JSON.stringify({ email, name, isAdmin }), + headers: { + "Content-Type": "application/json", + "Authorization": localStorage.getItem("usertoken") + }, + credentials: "include", + mode: "cors" + }); + + isAuthorised(response); + + return await response.status; +} + +export async function checkTheMailToken(mailToken){ + const response = await fetch("http://localhost:4000/auth/"+mailToken+"/check"); + const status = await response.json(); + return status === "USER_FOUND"; +} + +export async function fillUserPassword(pass, passrepeat, mailToken) { + const response = await fetch("http://localhost:4000/auth/secret", { + method: "POST", + body: JSON.stringify({ pass, passrepeat, mailToken }), + headers: { + "Content-Type": "application/json" + }, + credentials: "include", + mode: "cors" + }); + + isAuthorised(response); + + return await response.status; +} + +function isAuthorised(response){ + if (response.status === 401) { + throw new Error( + `HTTP Request went wrong: got "${response.statusText}"` + ); + } +} \ No newline at end of file diff --git a/programmero-frontend/src/functions/authenticationFunctions.js b/programmero-frontend/src/functions/authenticationFunctions.js new file mode 100644 index 0000000..19b7ed6 --- /dev/null +++ b/programmero-frontend/src/functions/authenticationFunctions.js @@ -0,0 +1,19 @@ +export async function login(user) { + let sendingBody = JSON.stringify({ + email: user.email, + password: user.password + }); + const url = "http://localhost:4000/auth/login"; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: sendingBody + }); + const json = await response.json(); + if (response.status !== 200) { + throw new Error(json.code); + } + return json; +} diff --git a/programmero-frontend/src/functions/validationFunctions.js b/programmero-frontend/src/functions/validationFunctions.js new file mode 100644 index 0000000..b191e7e --- /dev/null +++ b/programmero-frontend/src/functions/validationFunctions.js @@ -0,0 +1,15 @@ +import isEmpty from "lodash/isEmpty"; + +export const validateInput = (data)=>{ + let errors = {}; + if (data.email === ""){ + errors.email = "Email is vereist"; + } + if (data.password === ""){ + errors.password = "Wachtwoord is vereist"; + } + return{ + errors, + isValid: isEmpty(errors) + }; +}; \ No newline at end of file diff --git a/programmero-frontend/src/images/keyboard-key-enter.png b/programmero-frontend/src/images/keyboard-key-enter.png new file mode 100644 index 0000000..e1976fb Binary files /dev/null and b/programmero-frontend/src/images/keyboard-key-enter.png differ diff --git a/programmero-frontend/src/images/logo-programmero.svg b/programmero-frontend/src/images/logo-programmero.svg new file mode 100644 index 0000000..24dc1eb --- /dev/null +++ b/programmero-frontend/src/images/logo-programmero.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/programmero-frontend/src/index.js b/programmero-frontend/src/index.js new file mode 100644 index 0000000..a144c65 --- /dev/null +++ b/programmero-frontend/src/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import "bootstrap/dist/css/bootstrap.min.css"; +import { Provider } from "react-redux"; +import { compose, createStore, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; +import "./style/index.css"; +import App from "./components/App"; +import allReducers from "./reducers/_combineReducers"; + +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + +const store = createStore(allReducers, + composeEnhancers(applyMiddleware(thunk)), +); + +const mainComponent = (); + +ReactDOM.render(mainComponent, document.getElementById("root")); diff --git a/programmero-frontend/src/reducers/_combineReducers.js b/programmero-frontend/src/reducers/_combineReducers.js new file mode 100644 index 0000000..048b381 --- /dev/null +++ b/programmero-frontend/src/reducers/_combineReducers.js @@ -0,0 +1,19 @@ +import { combineReducers } from "redux"; +import appReducer from "./questionAddAndEditReducer"; +import authReducer from "./authReducer"; +import lessonManagementReducer from "./lessonManagementReducer"; +import questionListReducer from "./questionReadAndDeleteReducer"; +import studentReducer from "./studentReducer"; +import userReducer from "./userReducer"; + +const allReducers = combineReducers({ + appReducer, + authReducer, + lessonManagementReducer, + questionListReducer, + studentReducer, + userReducer + +}); + +export default allReducers; \ No newline at end of file diff --git a/programmero-frontend/src/reducers/authReducer.js b/programmero-frontend/src/reducers/authReducer.js new file mode 100644 index 0000000..b521480 --- /dev/null +++ b/programmero-frontend/src/reducers/authReducer.js @@ -0,0 +1,22 @@ +import { SET_USER } from "../actions/actionTypes"; +const initialState = { + isLoggedIn: false, + name: "", + email: "", + isAdmin: "" +}; +let newState; + +export default function (state = initialState, action) { + switch (action.type) { + case SET_USER: + newState = { ...state }; + newState.isAdmin = action.isAdmin; + newState.email = action.email; + newState.name = action.name; + newState.isLoggedIn = true; + return newState; + default: + return state; + } +} diff --git a/programmero-frontend/src/reducers/lessonManagementReducer.js b/programmero-frontend/src/reducers/lessonManagementReducer.js new file mode 100644 index 0000000..61ea27c --- /dev/null +++ b/programmero-frontend/src/reducers/lessonManagementReducer.js @@ -0,0 +1,52 @@ +//import types +import { CREATE_LESSON, SWITCH_LESSON_SELECTION, DELETE_SELECTED_LESSONS, CHANGE_SELECTED_LESSON_NAME, GET_LESSONS,RESET_STATUS_CODE } from "../actions/actionTypes"; + +const initState = { + lessons: [], + active: [] +}; + +let newLessons, element, newActive; +export default function (state = initState, action) { + switch (action.type) { + case CREATE_LESSON: + newLessons = [...state.lessons]; + newLessons.push({ + _id: action._id, + name: action.name, + description: action.description, + programmingLanguage: action.programmingLanguage + }); + return { ...state, lessons: newLessons,statusCode: action.errorCode }; + case SWITCH_LESSON_SELECTION: + element = state.active.find(lessonName => { + return lessonName === action.name; + }); + + if (typeof element === "string") { + newActive = state.active.filter(lessonName => lessonName !== element); + } else { + newActive = [...state.active, action.name]; + } + return { ...state, active: newActive }; + case DELETE_SELECTED_LESSONS: + newLessons = [...state.lessons]; + newLessons = newLessons.filter(lesson => { + return !state.active.includes(lesson.name); + }); + return { ...state, lessons: newLessons, active: [], statusCode: action.errorCode }; + case CHANGE_SELECTED_LESSON_NAME: + element = state.lessons.find(lesson => lesson.name === action.oldName); + element.name = action.newName; + return { ...state, active: [], statusCode: action.errorCode }; + case GET_LESSONS: + newLessons = action.lessons; + return { ...state, lessons: newLessons }; + + case RESET_STATUS_CODE: + state.statusCode = 0; + return {...state}; + default: + return state; + } +} \ No newline at end of file diff --git a/programmero-frontend/src/reducers/questionAddAndEditReducer.js b/programmero-frontend/src/reducers/questionAddAndEditReducer.js new file mode 100644 index 0000000..7f32988 --- /dev/null +++ b/programmero-frontend/src/reducers/questionAddAndEditReducer.js @@ -0,0 +1,75 @@ +import { + ADD_QUESTION, + ADD_ANSWER, + EDIT_ANSWER, + DELETE_ANSWER, + REFRESH_THE_REDUCER, + QUESTION_IN_EDITING, + ERROR, + GET_SPECIFIC_QUESTION +} from "../actions/actionTypes"; +const initialState = { + answers: [], + question: "", + inEditing: "", + error: null +}; + +let newState, newAnswers; + +export default function(state = initialState, action) { + switch (action.type) { + case ADD_QUESTION: + newState = { ...state }; + newState.error = null; + newState.question = action.question; + return newState; + case ADD_ANSWER: + newAnswers = [...state.answers]; + if (action.id === "") { + newAnswers.push(action.answer); + } else { + newAnswers[action.id] = action.answer; + } + newState = { ...state, answers: newAnswers, inEditing: "" }; + newState.error = null; + return newState; + case EDIT_ANSWER: + newState = { ...state, inEditing: action.answerId }; + newState.error = null; + return newState; + case DELETE_ANSWER: + newAnswers = [...state.answers]; + newAnswers.splice(action.id, 1); + newState = { ...state, answers: newAnswers, inEditing: "" }; + newState.error = null; + return newState; + case REFRESH_THE_REDUCER: + newState = {...state}; + newState.answers = []; + newState.inEditing = ""; + newState.question = ""; + newState.error = null; + return newState; + case QUESTION_IN_EDITING: + newState = {...state}; + newState.question = action.question.question; + newAnswers = action.question.parts; + newState.answers = newAnswers; + newState.error = null; + return newState; + case ERROR: + newState = {...state}; + newState.error = action.error; + return newState; + case GET_SPECIFIC_QUESTION: + newState = {...state}; + newState.question = action.data.question; + newAnswers = action.data.parts; + newState.answers = newAnswers; + return newState; + default: + state.error = ""; + return state; + } +} diff --git a/programmero-frontend/src/reducers/questionReadAndDeleteReducer.js b/programmero-frontend/src/reducers/questionReadAndDeleteReducer.js new file mode 100644 index 0000000..1b16889 --- /dev/null +++ b/programmero-frontend/src/reducers/questionReadAndDeleteReducer.js @@ -0,0 +1,92 @@ +import { + SELECT_ALL_QUESIONS, + ADD_ONE_QUESTION, + SET_CHECKBOXES_STATE, + CHANGE_CHECKBOX_STATE, + DELETE_SELECTED_QUESTION, + GET_QUESTION +} from "../actions/actionTypes"; +import { deleteQuestion } from "../data/questionData"; + +const initialState = { + questions: [], + lessonId: "" +}; + +let checkboxesState, checkBoxes, newState, questions; + +export default function(state = initialState, action) { + switch (action.type) { + case GET_QUESTION: + /* Get questions from backend*/ + newState = { ...state }; + newState.questions = action.questions; + newState.lessonId = action.lessonId; + newState.questions.forEach(question => { + question.checked = false; + question.parent = action.lessonId; + }); + return newState; + + case SET_CHECKBOXES_STATE: + newState = { ...state }; + newState.questions = action.checkboxesState; + return newState; + + case SELECT_ALL_QUESIONS: + checkboxesState = action.checkboxesState; + checkBoxes = action.checkBoxes; + checkboxesState.forEach((checkbox, i) => { + checkBoxes[i].checked = true; + checkbox.checked = true; + }); + newState = { ...state }; + newState.questions = checkboxesState; + return newState; + + case CHANGE_CHECKBOX_STATE: + newState = { ...state }; + state.questions.forEach(question => { + if (question._id === action.evt.target.id) { + question.checked = !question.checked; + } + newState.questions = state.questions; + }); + return newState; + + case DELETE_SELECTED_QUESTION: + newState = { ...state }; + newState.questionstoDelete = state.questions.filter(lesson => { + return lesson.checked === true; + }); + + newState.questions = state.questions.filter(lesson => { + return lesson.checked === false; + }); + + checkBoxes = action.checkBoxes; + newState.questions.forEach((checkbox, i) => { + checkBoxes[i].checked = false; + }); + + /* Backend delete*/ + newState.questionstoDelete.forEach(lesson => { + deleteQuestion(lesson.parent, lesson._id); + }); + return newState; + case ADD_ONE_QUESTION: + newState = { ...state }; + questions = [...state.questions]; + if (questions.length > 0) { + if (questions[0].parent === action.question.parent) { + questions.push(action.question); + questions[questions.length - 1].number = questions.length - 1; + } + } + newState.questions = questions; + return newState; + + default: + return state; + } +} diff --git a/programmero-frontend/src/reducers/studentReducer.js b/programmero-frontend/src/reducers/studentReducer.js new file mode 100644 index 0000000..e5d5cc6 --- /dev/null +++ b/programmero-frontend/src/reducers/studentReducer.js @@ -0,0 +1,61 @@ +import { GET_CODE_CARD, GET_CORRECT_ANSWER, GET_SCORE, GET_LESSONPROGRAM_SCORE, GET_LESSON, GET_END_LESSONRESULT_INFO } from "../actions/actionTypes"; +const initialState = { + lessonProgram: "SPD A", + question: "Voorbeeld vraag?", + answerParts: [], + correctAnswer: [], + studentAnswer: [], + correct: false, + score: 0, + lastQuestion: false, + numberOfQuestion: 0, + allQuestion: 0, + addedScore: 0, + lessonScore: 0, + amountCorrectQuestions: 0, + lesson: {} + +}; +let newState; + +export default function (state = initialState, action) { + switch (action.type) { + case GET_CODE_CARD: + newState = { ...state }; + newState.question = action.data.question; + newState.answerParts = action.data.parts; + newState.numberOfQuestion = action.data.indexCurrentQuestion + 1; + newState.allQuestion = action.data.amountQuestions; + newState.lastQuestion = action.data.indexCurrentQuestion + 1 === action.data.amountQuestions; + return newState; + case GET_CORRECT_ANSWER: + newState = { ...state }; + newState.correct = action.data.correct; + newState.correctAnswer = action.data.correctAnswer; + newState.studentAnswer = action.studentAnswer; + newState.addedScore = action.data.addedScore; + newState.score += action.data.newScore; + return newState; + case GET_SCORE: + newState = { ...state }; + newState.score = action.data; + return newState; + case GET_LESSONPROGRAM_SCORE: + newState = { ...state }; + newState.lessonScore = action.data; + return newState; + case GET_LESSON: + newState = { ...state }; + newState.lesson = action.data; + return newState; + case GET_END_LESSONRESULT_INFO: + newState = {...state}; + newState.lessonProgram = action.data.lessonName; + newState.amountQuestions = action.data.amountQuestions; + newState.lessonScore = action.data.score; + newState.amountCorrectQuestions = action.data.amountCorrect; + return newState; + default: + return state; + } +} diff --git a/programmero-frontend/src/reducers/userReducer.js b/programmero-frontend/src/reducers/userReducer.js new file mode 100644 index 0000000..0e98989 --- /dev/null +++ b/programmero-frontend/src/reducers/userReducer.js @@ -0,0 +1,26 @@ +import { CREATE_USER, FILL_USER_PASSWORD, RESET_USER_REDUCER } from "../actions/actionTypes"; + +const initialState = { + userSubmittedSuccesfull: 0, + passwordChangedSuccessfull: 0 +}; + +let newState; + +export default function (state = initialState, action) { + switch (action.type) { + case CREATE_USER: + if (action.statusCode !== 201) return newState = { ...state, userSubmittedUnSuccesfull: action.statusCode }; + newState = { ...state, userSubmittedSuccesfull: action.statusCode }; + return newState; + case FILL_USER_PASSWORD: + if (action.statusCode !== 200) return newState = { ...state, passwordChangedUnSuccessfull: action.statusCode }; + newState = { ...state, passwordChangedSuccessfull: action.statusCode }; + return newState; + case RESET_USER_REDUCER: + newState = { ...initialState }; + return newState; + default: + return state; + } +} \ No newline at end of file diff --git a/programmero-frontend/src/setupTests.js b/programmero-frontend/src/setupTests.js new file mode 100644 index 0000000..efe42cf --- /dev/null +++ b/programmero-frontend/src/setupTests.js @@ -0,0 +1,3 @@ +import { configure } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +configure({ adapter: new Adapter() }); \ No newline at end of file diff --git a/programmero-frontend/src/style/index.css b/programmero-frontend/src/style/index.css new file mode 100644 index 0000000..8f5c39e --- /dev/null +++ b/programmero-frontend/src/style/index.css @@ -0,0 +1,474 @@ +* { + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; +} + +.alert { + margin: 20px 0; + float: left; + width: 100%; +} + +div.modal-body > ul { + margin: 20px 0; +} + +.modal-header h5 { + font-weight: bold; +} + +body, #root, #root .content, .questionEditList { + height: 100%; +} + +.enterImage { + height: 30px; + opacity: 0.6; + margin: 5px 15px; +} + +.links,.links:hover{ + color:white; + text-decoration: none; + cursor: pointer; +} +.links-black,.links-black:hover{ + color:black; + text-decoration: none; +} + +#lessonSelector { + border-radius: 10px; + padding: 5px; +} + +.questionFeedback { + padding: 10px; + border: 1px solid black; + border-radius: 15px; +} + +.managementHead { + text-align: left; + padding: 2vh 0; + background-color: #FFD966; +} + +.managementHead h1, .managementHead h3 { + text-align: left; +} + +.loginForm { + padding: 5vh; +} + +.correctAnswer ul { + list-style-type: none; +} + +.feedbackSection ul li:first-child { + border-left: 2px solid #000000; +} +.feedbackSection ul li:last-child { + border-right: 2px solid #000000; +} + +.feedbackSection ul { + align-content: flex-start ; + background-color: #fff; + height: 25vh; + padding: 10px; + overflow-y: scroll; + list-style: none; + -ms-box-orient: horizontal; + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -moz-flex; + display: -webkit-flex; + display: flex; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; +} + +.feedbackSection ul li { + transition: 0.2s; + background-color: #ececec; + height: 40px; + line-height: 40px; + border-left: 2px dashed #ffffff; + border-top: 1px solid black; + border-bottom: 1px solid black; + margin: 5px 0; + padding: 0 10px; + text-align: center; +} + +.scoreFeedback { + text-align: right; + font-size: 2.5em; + color: #ffe557; + margin: 0 0 2vh 0; +} + +.success { + background-color: #CFDEC8 !important; +} + +.false { + background-color: #E06666 !important; +} + +.lessonprogramResult { + text-align: center; + background-color: #B6D7A8; + min-height: calc(100vh - 76.4px); + padding: 5vh; +} + +.correctAnswers { + max-width: 15vw; + margin: 2vh auto; +} + +.lessonprogramResult .titles { + margin: 0 0 5vh 0; +} + +.lessonprogramResult .scores { + font-size: 1.5em; + margin: 0 0 5vh 0; +} + +.resultGif { + max-width: 50vw; + margin: 5vh auto; +} + +.lessonprogramResult .scores .score { + color: #FED966; + margin: 0 0 2vh 0; + +} + +.questionEditList { + background-color: #FFD966; + min-height: calc(100vh - 76.4px); +} + +h1.pageTitle { + font-size: 1.8em; + padding: 5vh 0; +} + +.questionList { + height: auto; + margin: 0; +} + +.questionList tbody tr { + line-height: 25px; + min-height: 25px; + height: 50px; +} + +.tableContainer { + border-radius: 15px 0 0 15px; + background-color: #EFEFEF; + position: relative; + height: 60vh; + overflow-y: scroll; +} + +.answerField { + background-color: #E0E0E0; +} + +.lesprogrammas ul { + width: 100%; + padding: 0 15px 0 0; +} + +.lesprogrammas ul div button { + width: 100%; +} + +.addQuestion { + padding: 0 0 5vh 0; + background-color: #9FC5F8; + min-height:calc(100vh - 76.4px); +} +.answers { + align-content: flex-start ; + background-color: #fff; + height: 30vh; + padding: 10px; + overflow-y: scroll; + list-style: none; + -ms-box-orient: horizontal; + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -moz-flex; + display: -webkit-flex; + display: flex; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; +} + +.answer { + transition: 0.2s; + background-color: #ececec; + height: 40px; + line-height: 40px; + border-left: 2px dashed #a4a1a1; + border-top: 1px solid black; + border-bottom: 1px solid black; + margin: 5px 0; + padding: 0 10px; + text-align: center; +} + +.answer:first-child { + border-left: 2px solid #000000; +} +.answer:last-child { + border-right: 2px solid #000000; +} + +.jumbotron{ + margin-bottom: 0rem +} +/* ADMIN UI */ +.firstRow { + min-height: calc(50vh - 38.2px); +} + +.seccondRow { + min-height: calc(50vh - 38.2px); +} + +.lessonProgram { + background-color: #93C47D; +} + +.maintainQuestion { + background-color: #FFD966; +} + +.newQuestion { + background-color: #9FC5F8; +} + +.newUser { + background-color: #F9CB9C; +} + + +.buttonControl { + padding: 2vh 0; +} + +.homeItem { + height: 100% +} + +.jumbotron { + padding: 5rem 2rem; +} + +.tiltAnimation { + transition: 0.5s; +} + +.tiltAnimation:hover { + transition: 0.5s; + transform: rotate(15deg); +} + +.homeItem div svg { + transition: 0.5s; +} + +.homeItem:hover div svg { + transition: 0.5s; + transform: rotate(15deg); +} + +.pencilQuestion { + font-size: 2em; +} + +.linkWrap { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + border: none; +} + +#root > div > div.container-fluid > div > div> a > div > h2 { + color: black; +} + +.inEditing{ + transition: 0.2s; + border: 1px solid red; + margin: 5px; + background-color: white; + border-radius: 5px; + color:black; + z-index: 10; + + height: 55px; + line-height: 55px; + margin: 5px 0; + padding: 0 10px; +} + +.inEditing{ + border: 1px solid black; + margin: 5px; + background-color:rgb(226, 226, 226); + border-radius: 5px; + color:black; + z-index: 10; +} + +.scrollWindow { + height: 400px; + overflow-y: scroll; +} +.fadeIn { + -webkit-animation: fade 1s; + animation: fade 1s; + opacity: 1; + } +/* -fail- */ +@-webkit-keyframes fade { + from {opacity: 0} + to {opacity: 1} +}@keyframes fade { + from {opacity: 0} + to {opacity: 1} +} +.scrollWindow { + height: 400px; + overflow-y: scroll; +} + +.lesProgramma{ + text-align: center; + padding: 40px 10px; + border: black solid 2px; + border-radius: 5px; + width:80%; + margin: 5vh auto; + background: white; +} + +.lesprogramma-container{ + padding-top: 5vh; + background: #B6D7A8; + min-height: calc(100vh - 72.4px ); +} + +.card { + height: 40px; + float: left; + line-height: 25px; + display: inline; + margin: 5px 5px; + padding: 5px 10px; + cursor: pointer; +} +.card:hover{ + cursor: pointer; +} + + +.newUser { + background-color: #F9CB9C; + } + +.practiceTitle { + margin: 5vh 0; + float: left; + width: 100%; + background-color: #fff; + padding: 10px 20px; + border-radius: 10px; + font-size: 1.2em; +} + +.answerParts { + border: 1px dashed black; + align-content: flex-start; + min-height: 15vh; + padding: 20px 10px; +} + +.invite-user{ + background-color: #F9CB9C; + min-height: calc(100vh - 76.4px); +} + +.lesprogrammas { + max-height:300px; + overflow-y: scroll; + list-style: none; + -ms-box-orient: horizontal; + display: -moz-flex; + display: flex; + flex-wrap: wrap; +} + +.card { + transition: 0.1s; +} + +.gu-transit { + border: 1px dashed black; +} + + +/**************************** MEDIA QUERIES *****************************/ +@media only screen and (max-width: 900px) { + .correctAnswers { + max-width: 40vw; + } +} + +/* 404 page */ + + .bg-img { + position: absolute; + width: 100%; + height: 100vh; + background: url(http://www.reactiongifs.us/wp-content/uploads/2015/04/nothing_to_see_here_naked_gun.gif) no-repeat center center; + background-size: cover; + } + .h1 { + font-size: 160px; + color: white; + } + + .h2 { + margin-top: 0; + font-size: 30px; + color: white; + } + .center-404{ + width: 50%; + margin: 0 auto; + border-radius: 10px; + padding: 10px; + background-color:rgba(0, 0, 0, 0.5); + text-align: center; + margin-top: 20%; + } +