From 2f88001fd4bb28e813dea4aaf1378cfbabf7393f Mon Sep 17 00:00:00 2001 From: gilles-arnout Date: Sat, 2 Mar 2024 17:46:29 +0100 Subject: [PATCH 01/58] init and remove .env --- .env | 12 -- frontend/README.md | 2 +- frontend/package-lock.json | 255 +++++++++++------------ frontend/package.json | 14 +- frontend/pages/index.tsx | 14 ++ frontend/src/app/globals.css | 33 --- frontend/src/app/layout.tsx | 22 -- frontend/src/app/page.tsx | 37 ---- frontend/src/components/ui/LoginForm.tsx | 16 ++ frontend/src/components/ui/button.tsx | 56 +++++ frontend/src/lib/utils.ts | 6 + frontend/src/styles/globals.css | 76 +++++++ frontend/tailwind.config.ts | 86 ++++++-- 13 files changed, 371 insertions(+), 258 deletions(-) delete mode 100644 .env create mode 100644 frontend/pages/index.tsx delete mode 100644 frontend/src/app/globals.css delete mode 100644 frontend/src/app/layout.tsx delete mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/components/ui/LoginForm.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/styles/globals.css diff --git a/.env b/.env deleted file mode 100644 index 316039e1..00000000 --- a/.env +++ /dev/null @@ -1,12 +0,0 @@ -DEBUG=1 -SECRET_KEY=development_key -DJANGO_ALLOWED_HOSTS='localhost 127.0.0.1 [::1] django' -SQL_ENGINE=django.db.backends.postgresql -SQL_DATABASE=pigeonhole_dev -SQL_USER=pigeonhole -SQL_PASSWORD=password -SQL_HOST=db -SQL_PORT=5432 -DATABASE=postgres -DJANGO_SUPERUSER_PASSWORD=abc -DJANGO_SUPERUSER_EMAIL=abc@example.com \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index c4033664..08e12e04 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -16,7 +16,7 @@ bun dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +You can start editing the page by modifying `app/index.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d72dd96b..9d34338f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,15 @@ "name": "pigeonhole", "version": "0.1.0", "dependencies": { + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.344.0", "next": "14.1.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "tailwind-merge": "^2.2.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@types/node": "^20", @@ -37,7 +43,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "engines": { "node": ">=10" }, @@ -49,7 +54,6 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -150,7 +154,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -167,7 +170,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -179,7 +181,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -194,7 +195,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -208,7 +208,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -217,7 +216,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -225,14 +223,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -391,7 +387,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -404,7 +399,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -413,7 +407,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -426,12 +419,46 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz", @@ -465,13 +492,13 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.57", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.57.tgz", "integrity": "sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -491,7 +518,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", @@ -667,7 +694,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -676,7 +702,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -690,14 +715,12 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -709,8 +732,7 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" }, "node_modules/argparse": { "version": "2.0.1", @@ -968,14 +990,12 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } @@ -994,7 +1014,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1077,7 +1096,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "engines": { "node": ">= 6" } @@ -1121,7 +1139,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1145,7 +1162,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1153,16 +1169,42 @@ "node": ">= 6" } }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1173,14 +1215,12 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "engines": { "node": ">= 6" } @@ -1195,7 +1235,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1209,7 +1248,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -1221,7 +1259,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1298,8 +1336,7 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -1316,8 +1353,7 @@ "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "node_modules/doctrine": { "version": "3.0.0", @@ -1334,8 +1370,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { "version": "1.4.676", @@ -1346,8 +1381,7 @@ "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/enhanced-resolve": { "version": "5.15.0", @@ -1951,7 +1985,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1967,7 +2000,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1991,7 +2023,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -2012,7 +2043,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2069,7 +2099,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -2104,7 +2133,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2118,7 +2146,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2202,7 +2229,6 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -2224,7 +2250,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -2236,7 +2261,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2245,7 +2269,6 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2402,7 +2425,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2521,7 +2543,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -2561,7 +2582,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -2588,7 +2608,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2609,7 +2628,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -2633,7 +2651,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2666,7 +2683,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -2820,8 +2836,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/iterator.prototype": { "version": "1.1.2", @@ -2840,7 +2855,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -2858,7 +2872,6 @@ "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, "bin": { "jiti": "bin/jiti.js" } @@ -2969,7 +2982,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, "engines": { "node": ">=10" } @@ -2977,8 +2989,7 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-path": { "version": "6.0.0", @@ -3016,16 +3027,22 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, "engines": { "node": "14 || >=16.14" } }, + "node_modules/lucide-react": { + "version": "0.344.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", + "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -3034,7 +3051,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -3068,7 +3084,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -3083,7 +3098,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -3195,7 +3209,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3213,7 +3226,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3222,7 +3234,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "engines": { "node": ">= 6" } @@ -3427,7 +3438,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -3435,14 +3445,12 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -3472,7 +3480,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -3484,7 +3491,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3493,7 +3499,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "engines": { "node": ">= 6" } @@ -3511,7 +3516,6 @@ "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3539,7 +3543,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -3556,7 +3559,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -3575,7 +3577,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3610,7 +3611,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "dev": true, "engines": { "node": ">=14" }, @@ -3622,7 +3622,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -3641,7 +3640,6 @@ "version": "6.0.15", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", - "dev": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3653,8 +3651,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -3689,7 +3686,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -3738,7 +3734,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -3747,7 +3742,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -3779,8 +3773,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -3804,7 +3797,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -3839,7 +3831,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -3884,7 +3875,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -4009,7 +3999,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4021,7 +4010,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -4048,7 +4036,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -4085,7 +4072,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -4103,7 +4089,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4116,14 +4101,12 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -4135,7 +4118,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -4215,7 +4197,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4228,7 +4209,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4283,7 +4263,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -4317,7 +4296,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -4325,11 +4303,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.1.tgz", + "integrity": "sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==", + "dependencies": { + "@babel/runtime": "^7.23.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", - "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4362,6 +4351,14 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -4381,7 +4378,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -4390,7 +4386,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -4402,7 +4397,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -4425,8 +4419,7 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -4611,14 +4604,12 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -4709,7 +4700,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -4727,7 +4717,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4743,14 +4732,12 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4764,7 +4751,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -4776,7 +4762,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -4788,7 +4773,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -4815,7 +4799,6 @@ "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, "engines": { "node": ">= 14" } diff --git a/frontend/package.json b/frontend/package.json index 47c304b2..b5685b8f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,19 +9,25 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lucide-react": "^0.344.0", + "next": "14.1.0", "react": "^18", "react-dom": "^18", - "next": "14.1.0" + "tailwind-merge": "^2.2.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.0", "postcss": "^8", "tailwindcss": "^3.3.0", - "eslint": "^8", - "eslint-config-next": "14.1.0" + "typescript": "^5" } } diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx new file mode 100644 index 00000000..e7a1e184 --- /dev/null +++ b/frontend/pages/index.tsx @@ -0,0 +1,14 @@ +// pages/index.tsx +import React from 'react'; +import LoginForm from '@/components/ui/LoginForm'; + + +const Home = () => { + return ( +
+ +
+ ); +}; + +export default Home; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css deleted file mode 100644 index 875c01e8..00000000 --- a/frontend/src/app/globals.css +++ /dev/null @@ -1,33 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx deleted file mode 100644 index 3314e478..00000000 --- a/frontend/src/app/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx deleted file mode 100644 index 9fa72924..00000000 --- a/frontend/src/app/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client" - -import Image from "next/image"; -import React, { useState, useEffect } from 'react'; - - -export default function Home() { - - - const [data, setData] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('http://127.0.0.1:8000/groups/'); - if (response.ok) { - const json = await response.json(); - setData(json); - } else { - console.error('Failed to fetch data'); - } - } catch (error) { - console.error('Error:', error); - } - }; - - fetchData(); - }, []); - - - return ( -
-

Lijst van groepen als test van API:

-

{JSON.stringify(data)}

-
- ); -} diff --git a/frontend/src/components/ui/LoginForm.tsx b/frontend/src/components/ui/LoginForm.tsx new file mode 100644 index 00000000..b953d712 --- /dev/null +++ b/frontend/src/components/ui/LoginForm.tsx @@ -0,0 +1,16 @@ +// src/app/components/LoginForm.tsx + +import { Button } from "@/components/ui/button" + +const LoginForm = () => { + return ( +
+

Pigeonhole

+
+ +
+
+ ); +}; + +export default LoginForm; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 00000000..0ba42773 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 00000000..d084ccad --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css new file mode 100644 index 00000000..6a757250 --- /dev/null +++ b/frontend/src/styles/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index e9a0944e..84287e82 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,20 +1,80 @@ -import type { Config } from "tailwindcss"; +import type { Config } from "tailwindcss" -const config: Config = { +const config = { + darkMode: ["class"], content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", }, }, }, - plugins: [], -}; -export default config; + plugins: [require("tailwindcss-animate")], +} satisfies Config + +export default config \ No newline at end of file From b5b24f2b97fe6318cd4368bbd5563e410df1cbb6 Mon Sep 17 00:00:00 2001 From: gilles-arnout Date: Sat, 2 Mar 2024 18:16:37 +0100 Subject: [PATCH 02/58] basic login page --- frontend/pages/index.tsx | 10 ++--- frontend/src/components/ui/LoginForm.tsx | 52 ++++++++++++++++++---- frontend/src/components/ui/button.tsx | 56 ------------------------ frontend/src/lib/utils.ts | 6 +-- 4 files changed, 51 insertions(+), 73 deletions(-) delete mode 100644 frontend/src/components/ui/button.tsx diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index e7a1e184..6f4f5a52 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -4,11 +4,11 @@ import LoginForm from '@/components/ui/LoginForm'; const Home = () => { - return ( -
- -
- ); + return ( +
+ +
+ ); }; export default Home; diff --git a/frontend/src/components/ui/LoginForm.tsx b/frontend/src/components/ui/LoginForm.tsx index b953d712..5b0ef3a8 100644 --- a/frontend/src/components/ui/LoginForm.tsx +++ b/frontend/src/components/ui/LoginForm.tsx @@ -1,16 +1,50 @@ // src/app/components/LoginForm.tsx -import { Button } from "@/components/ui/button" +import React, {useState} from 'react'; const LoginForm = () => { - return ( -
-

Pigeonhole

-
- -
-
- ); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const handleLogin = () => { + // Implement your login logic here + }; + + const handleCASLogin = () => { + // Implement your CAS login logic here + }; + + return ( +
+

Pigeon Hole

+
+ +
+ +
+ + +
+
+ ); }; export default LoginForm; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx deleted file mode 100644 index 0ba42773..00000000 --- a/frontend/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index d084ccad..0189d300 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import {type ClassValue, clsx} from "clsx" +import {twMerge} from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)) } From 59393c70e98e0400bc41543bb9c05eed4e8b8d92 Mon Sep 17 00:00:00 2001 From: avoyen Date: Sat, 2 Mar 2024 20:47:38 +0100 Subject: [PATCH 03/58] First implementation of a view. Much work to do. --- backend/pigeonhole/apps/projects/views.py | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 backend/pigeonhole/apps/projects/views.py diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py new file mode 100644 index 00000000..0480004e --- /dev/null +++ b/backend/pigeonhole/apps/projects/views.py @@ -0,0 +1,35 @@ +from django.http import JsonResponse +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView + +from models import Project + + +class ProjectAPIView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, subject_id, project_id): + # TODO still figuring out how to implement views properly. + if request.user.teacher: + # TODO should be able to get every project as an admin, not as a teacher. (check the is_admin field) + # This happens when the user is a teacher or admin. + project = Project.objects.get(project_id=project_id) + return JsonResponse({"id": project.project_id, + "name": project.name, + "deadline": project.deadline, + "description": project.description}, status=200) + elif request.user.student: + # When the user is a student. + student = request.user.student + if student.projects.filter(project_id=project_id).exists(): + project = Project.objects.get(project_id=project_id) + + return JsonResponse({"id": project.project_id, + "name": project.name, + "deadline": project.deadline, + "description": project.description}, status=200) + else: + return JsonResponse({"message": "Project not found"}, status=404) + else: + # User isn't recognized. + return JsonResponse({"message": "User role not recognized"}, status=403) From 66e106185ee2e50ef7b33a32fcc10811a0c85ce0 Mon Sep 17 00:00:00 2001 From: avoyen Date: Sat, 2 Mar 2024 20:53:33 +0100 Subject: [PATCH 04/58] model manager and put function --- backend/pigeonhole/apps/projects/models.py | 1 + backend/pigeonhole/apps/projects/views.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/backend/pigeonhole/apps/projects/models.py b/backend/pigeonhole/apps/projects/models.py index fec3aa04..97a93276 100644 --- a/backend/pigeonhole/apps/projects/models.py +++ b/backend/pigeonhole/apps/projects/models.py @@ -6,6 +6,7 @@ # Create your models here. class Project(models.Model): + objects = models.Manager() project_id = models.BigAutoField(primary_key=True) course_id = models.ForeignKey(Course, on_delete=models.CASCADE) name = models.CharField(max_length=256) diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index 0480004e..ee962511 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -33,3 +33,13 @@ def get(self, request, subject_id, project_id): else: # User isn't recognized. return JsonResponse({"message": "User role not recognized"}, status=403) + + def put(self, request, subject_id, project_id): + if request.user.teacher: + # update project data, such as name, description, ... + project = Project.objects.get(project_id=project_id) + elif request.user.student: + student = request.user.student + project = Project.objects.get(project_id=project_id) + + From 8165e90fc6030586e382b6982e62dd43ee34912a Mon Sep 17 00:00:00 2001 From: robinpdev Date: Tue, 5 Mar 2024 14:43:45 +0100 Subject: [PATCH 05/58] add swagger ui on /swagger and remove .env from tracked files --- .env | 12 ------------ backend/pigeonhole/settings.py | 10 ++++++++++ backend/pigeonhole/urls.py | 29 ++++++++++++++++++++++++----- backend/requirements.txt | 5 ++++- 4 files changed, 38 insertions(+), 18 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 316039e1..00000000 --- a/.env +++ /dev/null @@ -1,12 +0,0 @@ -DEBUG=1 -SECRET_KEY=development_key -DJANGO_ALLOWED_HOSTS='localhost 127.0.0.1 [::1] django' -SQL_ENGINE=django.db.backends.postgresql -SQL_DATABASE=pigeonhole_dev -SQL_USER=pigeonhole -SQL_PASSWORD=password -SQL_HOST=db -SQL_PORT=5432 -DATABASE=postgres -DJANGO_SUPERUSER_PASSWORD=abc -DJANGO_SUPERUSER_EMAIL=abc@example.com \ No newline at end of file diff --git a/backend/pigeonhole/settings.py b/backend/pigeonhole/settings.py index 36aaecaa..4dda9d6e 100644 --- a/backend/pigeonhole/settings.py +++ b/backend/pigeonhole/settings.py @@ -48,6 +48,7 @@ 'backend.pigeonhole.apps.projects', 'backend.pigeonhole.apps.submissions', 'backend.pigeonhole.apps.groups', + 'drf_yasg', ] MIDDLEWARE = [ @@ -156,3 +157,12 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +SWAGGER_SETTINGS = { + 'SECURITY_DEFINITIONS': { + 'Basic': { + 'type': 'basic' + } + } +} \ No newline at end of file diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 2cb677dd..1e106798 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,17 +1,36 @@ from django.urls import include, path -from rest_framework import routers +from rest_framework import routers, permissions -from backend.testapi import views +from backend.testapi import views as test_views +#from backend.pigeonhole.apps.projects import views as project_views +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="My API", + default_version='v1', + description="My API description", + terms_of_service="https://www.example.com/terms/", + contact=openapi.Contact(email="contact@example.com"), + license=openapi.License(name="Awesome License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) router = routers.DefaultRouter() -router.register(r'users', views.UserViewSet) -router.register(r'groups', views.GroupViewSet) +router.register(r'users', test_views.UserViewSet) +router.register(r'groups', test_views.GroupViewSet) +#router.register(r'projects', project_views.ProjectAPIView) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), ] urlpatterns += router.urls diff --git a/backend/requirements.txt b/backend/requirements.txt index 3d0d187c..28ae5bb6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,4 +4,7 @@ djangorestframework-simplejwt~=5.2.2 djangorestframework~=3.14.0 flake8==7.0.0 psycopg2-binary~=2.9.5 -pytz~=2022.7.1 \ No newline at end of file +pytz~=2022.7.1 +pyyaml==6.0.1 +uritemplate==4.1.1 +drf-yasg==1.21.7 From 546f63b3c6ebc5c9123cbc5a90426ba0ae4ecf2b Mon Sep 17 00:00:00 2001 From: Reinhard Date: Thu, 7 Mar 2024 21:57:17 +0100 Subject: [PATCH 06/58] course api --- backend/pigeonhole/apps/courses/views.py | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 backend/pigeonhole/apps/courses/views.py diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py new file mode 100644 index 00000000..2a69e8ec --- /dev/null +++ b/backend/pigeonhole/apps/courses/views.py @@ -0,0 +1,48 @@ +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from .models import Course, CourseSerializer + + +class CourseViewSet(viewsets.ModelViewSet): + queryset = Course.objects.all() + serializer_class = CourseSerializer + permission_classes = [IsAuthenticated] + + def create(self, request, *args, **kwargs): + serializer = CourseSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = CourseSerializer(instance, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + def list(self, request, *args, **kwargs): + serializer = CourseSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + serializer = CourseSerializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = CourseSerializer(instance, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 2baf464d40359d9e7505312fc5865d7363aac70c Mon Sep 17 00:00:00 2001 From: avoyen Date: Fri, 8 Mar 2024 18:45:48 +0100 Subject: [PATCH 07/58] Project viewset and custom permission class --- .../pigeonhole/apps/projects/permissions.py | 20 +++++ backend/pigeonhole/apps/projects/views.py | 87 ++++++++++--------- 2 files changed, 66 insertions(+), 41 deletions(-) create mode 100644 backend/pigeonhole/apps/projects/permissions.py diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py new file mode 100644 index 00000000..e03e7af6 --- /dev/null +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -0,0 +1,20 @@ +from rest_framework import permissions + + +class CanAccessProject(permissions.BasePermission): + # Custom permission class to determine if the currect user has access + # to the project data. + def has_permission(self, request, view): + if request.user.is_authenticated: + # If the user is a teacher, grant access + if request.user.teacher or request.user.teacher.is_admin: + return True + # If the user is a student, grant access only to their own projects + elif request.user.student: + subject_id = view.kwargs.get('subject_id') + project_id = view.kwargs.get('pk') + # TODO check if the student is subscribed to project + student = request.user.student + if student.course.filter(id=subject_id).exists(): + return True + return False diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index ee962511..5b722fd7 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -1,45 +1,50 @@ -from django.http import JsonResponse +from rest_framework import status +from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated -from rest_framework.views import APIView - -from models import Project - - -class ProjectAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request, subject_id, project_id): - # TODO still figuring out how to implement views properly. - if request.user.teacher: - # TODO should be able to get every project as an admin, not as a teacher. (check the is_admin field) - # This happens when the user is a teacher or admin. - project = Project.objects.get(project_id=project_id) - return JsonResponse({"id": project.project_id, - "name": project.name, - "deadline": project.deadline, - "description": project.description}, status=200) - elif request.user.student: - # When the user is a student. - student = request.user.student - if student.projects.filter(project_id=project_id).exists(): - project = Project.objects.get(project_id=project_id) - - return JsonResponse({"id": project.project_id, - "name": project.name, - "deadline": project.deadline, - "description": project.description}, status=200) - else: - return JsonResponse({"message": "Project not found"}, status=404) - else: - # User isn't recognized. - return JsonResponse({"message": "User role not recognized"}, status=403) +from rest_framework.response import Response + +from models import Project, ProjectSerializer +from permissions import CanAccessProject - def put(self, request, subject_id, project_id): - if request.user.teacher: - # update project data, such as name, description, ... - project = Project.objects.get(project_id=project_id) - elif request.user.student: - student = request.user.student - project = Project.objects.get(project_id=project_id) +class ProjectViewSet(viewsets.ModelViewSet): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + permission_classes = [IsAuthenticated & CanAccessProject] + def create(self, request, *args, **kwargs): + serializer = ProjectSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + subject_id = kwargs.get('subject_id') + project_id = kwargs.get('pk') + project = Project.objects.get(pk=project_id) + project.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + def retrieve(self, request, *args, **kwargs): + subject_id = kwargs.get('subject_id') + project_id = kwargs.get('pk') + + serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request, *args, **kwargs): + subject_id = kwargs.get('subject_id') + project_id = kwargs.get('pk') + project = Project.objects.get(pk=project_id) + serializer = ProjectSerializer(project, data=request.data) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + def archive(self, request, *args, **kwargs): + # Not sure what to do here yet + subject_id = kwargs.get('subject_id') + project_id = kwargs.get('pk') + serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) + return Response(serializer, status=status.HTTP_200_OK) From f224e8a62cfe730d3d4478a8e481a603d1522b48 Mon Sep 17 00:00:00 2001 From: Thibaud Collyn Date: Fri, 8 Mar 2024 22:33:43 +0100 Subject: [PATCH 08/58] Added Material UI dependence and tested it out with some buttons. --- frontend/package-lock.json | 802 ++++++++++++++++++++++- frontend/package.json | 5 + frontend/pages/index.tsx | 2 + frontend/src/components/ui/LoginCard.tsx | 17 + 4 files changed, 810 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/ui/LoginCard.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9d34338f..92e63922 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,11 @@ "name": "pigeonhole", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.12", + "@mui/icons-material": "^5.15.12", + "@mui/material": "^5.15.12", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -50,6 +55,186 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/runtime": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", @@ -61,6 +246,152 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -117,6 +448,45 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@fontsource/roboto": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.12.tgz", + "integrity": "sha512-x0o17jvgoSSbS9OZnUX2+xJmVRvVCfeaYJjkS7w62iN7CuJWtMf5vJj8LqgC7ibqIkitOHVW+XssRjgrcHn62g==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -234,6 +604,261 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.38.tgz", + "integrity": "sha512-AsjD6Y1X5A1qndxz8xCcR8LDqv31aiwlgWMPxFAX/kCKiIGKlK65yMeVZ62iQr/6LBz+9hSKLiD1i4TZdAHKcQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.12", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.12.tgz", + "integrity": "sha512-brRO+tMFLpGyjEYHrX97bzqeF6jZmKpqqe1rY0LyIHAwP6xRVzh++zSecOQorDOCaZJg4XkGT9xfD+RWOWxZBA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.12.tgz", + "integrity": "sha512-3BXiDlOd3AexZoEXa/VqpIpVIvosCzjLHsdMWzKMXbZdnBiJjmb9ECdqfjn5SpTClO49qvkKLhkTqdBH3fSFGw==", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.12.tgz", + "integrity": "sha512-vXJGg6KNKucsvbW6l7w9zafnpOp0CWc0Wx4mDykuABTpQ5QQBnZxP7+oB4yAS1hDZQ1WobbeIl0CjxK4EEahkA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.38", + "@mui/core-downloads-tracker": "^5.15.12", + "@mui/system": "^5.15.12", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.12", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/private-theming": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.12.tgz", + "integrity": "sha512-cqoSo9sgA5HE+8vZClbLrq9EkyOnYysooepi5eKaKvJ41lReT2c5wOZAeDDM1+xknrMDos+0mT2zr3sZmUiRRA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.12", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.11.tgz", + "integrity": "sha512-So21AhAngqo07ces4S/JpX5UaMU2RHXpEA6hNzI6IQjd/1usMPxpgK8wkGgTe3JKmC2KDmH8cvoycq5H3Ii7/w==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.12.tgz", + "integrity": "sha512-/pq+GO6yN3X7r3hAwFTrzkAh7K1bTF5r8IzS79B9eyKJg7v6B/t4/zZYMR6OT9qEPtwf6rYN2Utg1e6Z7F1OgQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.12", + "@mui/styled-engine": "^5.15.11", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.12", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.12", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.12.tgz", + "integrity": "sha512-8SDGCnO2DY9Yy+5bGzu00NZowSDtuyHP4H8gunhHGQoIlhlY2Z3w64wBzAOLpYw/ZhJNzksDTnS/i8qdJvxuow==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/@next/env": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", @@ -424,6 +1049,15 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -488,17 +1122,20 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "devOptional": true + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.2.57", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.57.tgz", "integrity": "sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -514,11 +1151,18 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "devOptional": true + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@typescript-eslint/parser": { "version": "6.21.0", @@ -987,6 +1631,20 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1087,7 +1745,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1231,6 +1888,34 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1258,8 +1943,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1367,6 +2051,15 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1396,6 +2089,14 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz", @@ -1557,7 +2258,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -2050,6 +2750,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2432,6 +3137,14 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2445,7 +3158,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2512,6 +3224,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -2899,6 +3616,11 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3408,7 +4130,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -3416,6 +4137,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3466,7 +4204,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -3666,7 +4403,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3727,8 +4463,22 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } }, "node_modules/read-cache": { "version": "1.0.0", @@ -3813,7 +4563,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -4052,6 +4801,14 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -4259,6 +5016,11 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -4393,6 +5155,14 @@ "node": ">=0.8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index b5685b8f..81adcf9f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,11 @@ "lint": "next lint" }, "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.12", + "@mui/icons-material": "^5.15.12", + "@mui/material": "^5.15.12", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 6f4f5a52..ae5ebfeb 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,12 +1,14 @@ // pages/index.tsx import React from 'react'; import LoginForm from '@/components/ui/LoginForm'; +import LoginCard from '@/components/ui/LoginCard'; const Home = () => { return (
+
); }; diff --git a/frontend/src/components/ui/LoginCard.tsx b/frontend/src/components/ui/LoginCard.tsx new file mode 100644 index 00000000..541689d5 --- /dev/null +++ b/frontend/src/components/ui/LoginCard.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; + +export default function ContainedButtons() { + return ( + + + + + + ); +} \ No newline at end of file From d1aaba84317d965a175b939e96d8e70745d5aa10 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Sat, 9 Mar 2024 10:54:53 +0100 Subject: [PATCH 09/58] corrected instances --- backend/pigeonhole/apps/courses/views.py | 25 ++++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py index 2a69e8ec..81880e95 100644 --- a/backend/pigeonhole/apps/courses/views.py +++ b/backend/pigeonhole/apps/courses/views.py @@ -1,6 +1,6 @@ from rest_framework import viewsets, status -from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from .models import Course, CourseSerializer @@ -18,16 +18,18 @@ def create(self, request, *args, **kwargs): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def update(self, request, *args, **kwargs): - instance = self.get_object() - serializer = CourseSerializer(instance, data=request.data) + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + serializer = CourseSerializer(course, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, *args, **kwargs): - instance = self.get_object() - instance.delete() + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + course.delete() return Response(status=status.HTTP_204_NO_CONTENT) def list(self, request, *args, **kwargs): @@ -35,14 +37,7 @@ def list(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - serializer = CourseSerializer(instance) + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + serializer = CourseSerializer(course, many=False) return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, *args, **kwargs): - instance = self.get_object() - serializer = CourseSerializer(instance, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From d30a0aa711ad3d9da5dd28de5540e0d2131c9abc Mon Sep 17 00:00:00 2001 From: Reinhard Date: Sat, 9 Mar 2024 11:30:39 +0100 Subject: [PATCH 10/58] added permissions --- .../pigeonhole/apps/courses/permissions.py | 34 +++++++++++++++++++ backend/pigeonhole/apps/courses/views.py | 3 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 backend/pigeonhole/apps/courses/permissions.py diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py new file mode 100644 index 00000000..8c93264c --- /dev/null +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -0,0 +1,34 @@ +from rest_framework import permissions +from backend.pigeonhole.apps.users.models import Student, Teacher + + +class CourseUserPermissions(permissions.BasePermission): + def has_permission(self, request, view): + if not request.user.is_authenticated: + return False + + if request.user.is_admin: + return True + + if request.user.is_teacher: + return True + + if request.user.is_student: + return view.action in ['list', 'retrieve'] + + return False + + def has_object_permission(self, request, view, obj): + if not request.user.is_authenticated: + return False + + if request.user.is_admin: + return True + + if request.user.is_teacher: + return Teacher.objects.filter(id=request.user.id, course=obj).exists() + + if request.user.is_student: + return Student.objects.filter(id=request.user.id, course=obj).exists() and view.action in ['list', 'retrieve'] + + return False diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py index 81880e95..fc1715a7 100644 --- a/backend/pigeonhole/apps/courses/views.py +++ b/backend/pigeonhole/apps/courses/views.py @@ -3,12 +3,13 @@ from rest_framework.response import Response from .models import Course, CourseSerializer +from .permissions import CourseUserPermissions class CourseViewSet(viewsets.ModelViewSet): queryset = Course.objects.all() serializer_class = CourseSerializer - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated, CourseUserPermissions] def create(self, request, *args, **kwargs): serializer = CourseSerializer(data=request.data) From 470806c32b0906b21d55c233e029f44588d4b272 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Sat, 9 Mar 2024 11:42:42 +0100 Subject: [PATCH 11/58] added courses to urls --- backend/pigeonhole/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 1e106798..29086460 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -2,6 +2,7 @@ from rest_framework import routers, permissions from backend.testapi import views as test_views +from backend.pigeonhole.apps.courses.views import CourseViewSet #from backend.pigeonhole.apps.projects import views as project_views from drf_yasg.views import get_schema_view from drf_yasg import openapi @@ -22,6 +23,7 @@ router = routers.DefaultRouter() router.register(r'users', test_views.UserViewSet) router.register(r'groups', test_views.GroupViewSet) +router.register(r'courses', CourseViewSet) #router.register(r'projects', project_views.ProjectAPIView) # Wire up our API using automatic URL routing. From fb2692fc2a7336c55ac4d35764b129daca2b8de0 Mon Sep 17 00:00:00 2001 From: gilles-arnout Date: Sat, 9 Mar 2024 14:25:58 +0100 Subject: [PATCH 12/58] styled login page --- frontend/pages/_document.js | 26 ++++ frontend/pages/index.tsx | 3 +- frontend/src/components/ui/LoginForm.tsx | 147 +++++++++++++++++------ frontend/src/lib/utils.ts | 2 + 4 files changed, 139 insertions(+), 39 deletions(-) create mode 100644 frontend/pages/_document.js diff --git a/frontend/pages/_document.js b/frontend/pages/_document.js new file mode 100644 index 00000000..c732e018 --- /dev/null +++ b/frontend/pages/_document.js @@ -0,0 +1,26 @@ +import Document, {Head, Html, Main, NextScript} from 'next/document'; +import {PRIMARY_COLOR} from "../src/lib/utils"; + +class MyDocument extends Document { + render() { + return ( + + + {/* PWA primary color */} + + + {/* Add other head elements here as needed */} + + +
+ + + + ); + } +} + +export default MyDocument; diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index ae5ebfeb..55d46771 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,14 +1,13 @@ // pages/index.tsx import React from 'react'; -import LoginForm from '@/components/ui/LoginForm'; import LoginCard from '@/components/ui/LoginCard'; +import LoginForm from '@/components/ui/LoginForm'; const Home = () => { return (
-
); }; diff --git a/frontend/src/components/ui/LoginForm.tsx b/frontend/src/components/ui/LoginForm.tsx index 5b0ef3a8..b84149bf 100644 --- a/frontend/src/components/ui/LoginForm.tsx +++ b/frontend/src/components/ui/LoginForm.tsx @@ -1,49 +1,122 @@ -// src/app/components/LoginForm.tsx - import React, {useState} from 'react'; +import {Box, Button, Container, CssBaseline, TextField, Typography} from '@mui/material'; +import {createTheme, ThemeProvider} from '@mui/material/styles'; +import SchoolIcon from '@mui/icons-material/School'; + +const theme = createTheme({ + palette: { + background: { + default: '#f4f5fd' + }, + primary: { + main: '#1976d2', + }, + secondary: { + main: '#9c27b0', + }, + }, + typography: { + fontFamily: 'Quicksand, sans-serif', + h4: { + fontWeight: 700, + }, + }, + components: { + MuiTextField: { + defaultProps: { + InputLabelProps: { + shrink: true, + }, + margin: 'normal', + required: true, + fullWidth: true, + }, + }, + MuiButton: { + defaultProps: { + variant: 'contained', + color: 'primary', + fullWidth: true, + style: {margin: '10px 0'}, + }, + }, + }, +}); -const LoginForm = () => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); +const LoginForm: React.FC = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); - const handleLogin = () => { + const handleLogin = (): void => { // Implement your login logic here + console.log('Login with:', email, password); }; - const handleCASLogin = () => { - // Implement your CAS login logic here + const handleCASLogin = (): void => { + // Implement CAS login logic here + console.log('Login with CAS'); }; return ( -
-

Pigeon Hole

-
- -
- -
- - -
-
+ + + + + + Pigeonhole + +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + + OR + + + + +
+
+
); }; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 0189d300..c8ec6dab 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -4,3 +4,5 @@ import {twMerge} from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export const PRIMARY_COLOR = "#1976d2"; From e0e423514e20ef96d082cb770b7fea75af528ced Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Sat, 9 Mar 2024 15:48:30 +0100 Subject: [PATCH 13/58] automatic submission number, student max in 1 group for every project --- backend/pigeonhole/apps/groups/models.py | 15 ++++++++++++++- backend/pigeonhole/apps/submissions/models.py | 16 +++++++++++++--- .../tests/test_models/test_submissions.py | 12 ------------ backend/pigeonhole/tests/test_views/__init__.py | 0 4 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 backend/pigeonhole/tests/test_views/__init__.py diff --git a/backend/pigeonhole/apps/groups/models.py b/backend/pigeonhole/apps/groups/models.py index 9f57317e..5e54a528 100644 --- a/backend/pigeonhole/apps/groups/models.py +++ b/backend/pigeonhole/apps/groups/models.py @@ -1,5 +1,6 @@ from django.db import models from rest_framework import serializers +from django.core.exceptions import ValidationError from backend.pigeonhole.apps.projects.models import Project from backend.pigeonhole.apps.users.models import Student @@ -15,12 +16,24 @@ class Group(models.Model): objects = models.Manager() + # a student can only be in one group per project + def clean(self): + for student in self.student.all(): + existing_groups = Group.objects.filter( + project_id=self.project_id, student=student).exclude( + group_id=self.group_id) + if existing_groups.exists(): + raise ValidationError(f"Student {student} is already part of " + "another group in this project.") + + # a student can only be in one group per project, group_nr is + # automatically assigned and unique per project def save(self, *args, **kwargs): if not self.group_id: if self.group_nr is None: max_group_nr = Group.objects.filter( project_id=self.project_id).aggregate( - models.Max('group_nr'))['group_nr__max'] or 0 + models.Max('group_nr'))['group_nr__max'] or 0 self.group_nr = max_group_nr + 1 super().save(*args, **kwargs) diff --git a/backend/pigeonhole/apps/submissions/models.py b/backend/pigeonhole/apps/submissions/models.py index 7025fb02..e8bb5593 100644 --- a/backend/pigeonhole/apps/submissions/models.py +++ b/backend/pigeonhole/apps/submissions/models.py @@ -1,5 +1,6 @@ from django.db import models from rest_framework import serializers +from django.core.exceptions import ValidationError from backend.pigeonhole.apps.groups.models import Group @@ -10,16 +11,25 @@ class Submissions(models.Model): group_id = models.ForeignKey(Group, on_delete=models.CASCADE, blank=False) submission_nr = models.IntegerField() file = models.FileField(upload_to='uploads/submissions/files/' + - str(group_id) + '/' + str(submission_nr) + '/', + str(group_id) + '/' + str(submission_nr) + '/', null=True, blank=False, max_length=255) timestamp = models.DateTimeField(auto_now_add=True) output_test = models.FileField(upload_to='uploads/submissions/outputs/' + - str(group_id) + '/' + str(submission_nr) + - '/output_test/', null=True, blank=False, + str(group_id) + '/' + str(submission_nr) + + '/output_test/', null=True, blank=False, max_length=255) objects = models.Manager() + # submission_nr is automatically assigned and unique per group, and + # increments + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if not self.submission_id: + max_submission_nr = Submissions.objects.filter( + group_id=self.group_id).aggregate( + models.Max('submission_nr'))['submission_nr__max'] or 0 + self.submission_nr = max_submission_nr + 1 + super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields) class SubmissionsSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/pigeonhole/tests/test_models/test_submissions.py b/backend/pigeonhole/tests/test_models/test_submissions.py index c6efed08..e9f0b16d 100644 --- a/backend/pigeonhole/tests/test_models/test_submissions.py +++ b/backend/pigeonhole/tests/test_models/test_submissions.py @@ -51,7 +51,6 @@ def setUp(self): # Create submission Submissions.objects.create( group_id=group, - submission_nr=1, ) def test_submission_student_relation(self): @@ -66,17 +65,6 @@ def test_submission_project_relation(self): project = submission.group_id.project_id self.assertEqual(submission.group_id.project_id, project) - def test_update_and_delete_submission(self): - submission = Submissions.objects.get(submission_nr=1) - submission.submission_nr = 2 - submission.save() - updated_submission = Submissions.objects.get(submission_nr=2) - self.assertEqual(updated_submission.submission_nr, 2) - - submission.delete() - with self.assertRaises(Submissions.DoesNotExist): - Submissions.objects.get(submission_nr=2) - def test_submission_file_upload_and_retrieval(self): submission = Submissions.objects.get(submission_nr=1) diff --git a/backend/pigeonhole/tests/test_views/__init__.py b/backend/pigeonhole/tests/test_views/__init__.py new file mode 100644 index 00000000..e69de29b From 008aecccce818d5a336f31217a0756a5896cd64f Mon Sep 17 00:00:00 2001 From: robinpdev Date: Sat, 9 Mar 2024 17:07:00 +0100 Subject: [PATCH 14/58] add admin panel --- backend/pigeonhole/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 29086460..16c3d037 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,4 +1,5 @@ from django.urls import include, path +from django.contrib import admin from rest_framework import routers, permissions from backend.testapi import views as test_views @@ -33,6 +34,7 @@ path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path("admin/", admin.site.urls), ] urlpatterns += router.urls From ada320962e1d36728ce9201d104ff5beb7e399a5 Mon Sep 17 00:00:00 2001 From: avoyen Date: Sat, 9 Mar 2024 19:42:48 +0100 Subject: [PATCH 15/58] fixed permission class, added url --- .../pigeonhole/apps/projects/permissions.py | 25 +++++++++++-------- backend/pigeonhole/apps/projects/views.py | 12 ++++----- backend/pigeonhole/urls.py | 3 ++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index e03e7af6..d08a56b5 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -1,20 +1,23 @@ from rest_framework import permissions +from backend.pigeonhole.apps.users.models import Teacher, Student class CanAccessProject(permissions.BasePermission): # Custom permission class to determine if the currect user has access # to the project data. def has_permission(self, request, view): - if request.user.is_authenticated: - # If the user is a teacher, grant access - if request.user.teacher or request.user.teacher.is_admin: + user = request.user + subject_id = view.kwargs.get('course_id') + # If the user is a teacher, grant access. + if isinstance(user, Teacher): + if user.course.filter(id=subject_id).exists(): return True - # If the user is a student, grant access only to their own projects - elif request.user.student: - subject_id = view.kwargs.get('subject_id') - project_id = view.kwargs.get('pk') - # TODO check if the student is subscribed to project - student = request.user.student - if student.course.filter(id=subject_id).exists(): - return True + elif isinstance(user, Teacher) and user.is_admin: + return True + # If the user is a student, grant access only to their own projects. + elif isinstance(user, Student): + if user.course.filter(id=subject_id).exists(): + return True + elif request.user.is_superuser: + return True return False diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index 5b722fd7..692c2cfa 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -3,8 +3,8 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from models import Project, ProjectSerializer -from permissions import CanAccessProject +from .models import Project, ProjectSerializer +from .permissions import CanAccessProject class ProjectViewSet(viewsets.ModelViewSet): @@ -21,21 +21,21 @@ def create(self, request, *args, **kwargs): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, *args, **kwargs): - subject_id = kwargs.get('subject_id') + course_id = kwargs.get('course_id') project_id = kwargs.get('pk') project = Project.objects.get(pk=project_id) project.delete() return Response(status=status.HTTP_204_NO_CONTENT) def retrieve(self, request, *args, **kwargs): - subject_id = kwargs.get('subject_id') + course_id = kwargs.get('course_id') project_id = kwargs.get('pk') serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) return Response(serializer.data, status=status.HTTP_200_OK) def update(self, request, *args, **kwargs): - subject_id = kwargs.get('subject_id') + course_id = kwargs.get('course_id') project_id = kwargs.get('pk') project = Project.objects.get(pk=project_id) serializer = ProjectSerializer(project, data=request.data) @@ -44,7 +44,7 @@ def update(self, request, *args, **kwargs): def archive(self, request, *args, **kwargs): # Not sure what to do here yet - subject_id = kwargs.get('subject_id') + course_id = kwargs.get('course_id') project_id = kwargs.get('pk') serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) return Response(serializer, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 29086460..b08a5df0 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -3,6 +3,7 @@ from backend.testapi import views as test_views from backend.pigeonhole.apps.courses.views import CourseViewSet +from backend.pigeonhole.apps.projects.views import ProjectViewSet #from backend.pigeonhole.apps.projects import views as project_views from drf_yasg.views import get_schema_view from drf_yasg import openapi @@ -24,7 +25,7 @@ router.register(r'users', test_views.UserViewSet) router.register(r'groups', test_views.GroupViewSet) router.register(r'courses', CourseViewSet) -#router.register(r'projects', project_views.ProjectAPIView) +router.register(r'courses//project/', ProjectViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. From fa9f4229cced1ce6793a95248d19b1299065cede Mon Sep 17 00:00:00 2001 From: avoyen Date: Sat, 9 Mar 2024 20:09:50 +0100 Subject: [PATCH 16/58] new url for creating project, added field to project model --- backend/pigeonhole/apps/projects/models.py | 1 + backend/pigeonhole/urls.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/pigeonhole/apps/projects/models.py b/backend/pigeonhole/apps/projects/models.py index 2621223d..f651f5bd 100644 --- a/backend/pigeonhole/apps/projects/models.py +++ b/backend/pigeonhole/apps/projects/models.py @@ -11,6 +11,7 @@ class Project(models.Model): course_id = models.ForeignKey(Course, on_delete=models.CASCADE) name = models.CharField(max_length=256) description = models.TextField() + deadline = models.DateTimeField() visible = models.BooleanField(default=False) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 99bc70b4..7a860619 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -26,7 +26,7 @@ router.register(r'users', test_views.UserViewSet) router.register(r'groups', test_views.GroupViewSet) router.register(r'courses', CourseViewSet) -router.register(r'courses//project/', ProjectViewSet) +router.register(r'courses//projects/', ProjectViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. @@ -36,6 +36,7 @@ path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), path("admin/", admin.site.urls), + path('courses//projects/', ProjectViewSet.as_view({'post': 'create'}), name='project-create'), ] urlpatterns += router.urls From 25e4146d3d369806c7048f2022ce61f19758d5a0 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Sat, 9 Mar 2024 22:02:51 +0100 Subject: [PATCH 17/58] course permissions fixed --- .../pigeonhole/apps/courses/permissions.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py index 8c93264c..201b666e 100644 --- a/backend/pigeonhole/apps/courses/permissions.py +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -4,31 +4,27 @@ class CourseUserPermissions(permissions.BasePermission): def has_permission(self, request, view): - if not request.user.is_authenticated: - return False - - if request.user.is_admin: + if request.user.is_superuser: return True - - if request.user.is_teacher: + if isinstance(request.user, Teacher): return True - if request.user.is_student: + if isinstance(request.user, Student): return view.action in ['list', 'retrieve'] return False def has_object_permission(self, request, view, obj): - if not request.user.is_authenticated: - return False - - if request.user.is_admin: + if request.user.is_superuser: return True + if isinstance(request.user, Teacher): + if request.user.is_admin: + return True + elif Teacher.objects.filter(id=request.user.id, course=obj).exists(): + return True + return view.action in ['list', 'retrieve'] - if request.user.is_teacher: - return Teacher.objects.filter(id=request.user.id, course=obj).exists() - - if request.user.is_student: - return Student.objects.filter(id=request.user.id, course=obj).exists() and view.action in ['list', 'retrieve'] + if isinstance(request.user, Student): + return view.action in ['list', 'retrieve'] return False From fd43ce853edd35a7acdd23b9e853983db639ae93 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Sun, 10 Mar 2024 04:40:52 +0100 Subject: [PATCH 18/58] auth backend (serializers + viewsets) --- backend/pigeonhole/apps/users/models.py | 19 --------- backend/pigeonhole/apps/users/serializers.py | 41 ++++++++++++++++++++ backend/pigeonhole/apps/users/viewsets.py | 24 ++++++++++++ backend/pigeonhole/settings.py | 7 +++- 4 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 backend/pigeonhole/apps/users/serializers.py create mode 100644 backend/pigeonhole/apps/users/viewsets.py diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index 9a0f36f4..cf022ecf 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -1,6 +1,5 @@ from django.contrib.auth.models import AbstractUser from django.db import models -from rest_framework import serializers from backend.pigeonhole.apps.courses.models import Course @@ -14,12 +13,6 @@ def name(self): return f"{self.first_name.strip()} {self.last_name.strip()}" -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ['id', 'e_mail', 'first_name', 'last_name'] - - class Student(models.Model): id = models.ForeignKey(User, on_delete=models.CASCADE, primary_key=True) number = models.IntegerField() @@ -28,12 +21,6 @@ class Student(models.Model): objects = models.Manager() -class StudentSerializer(serializers.ModelSerializer): - class Meta: - model = Student - fields = ['number', 'course', 'id'] - - class Teacher(models.Model): id = models.ForeignKey(User, on_delete=models.CASCADE, primary_key=True) course = models.ManyToManyField(Course) @@ -41,9 +28,3 @@ class Teacher(models.Model): is_assistant = models.BooleanField(default=False) objects = models.Manager() - - -class TeacherSerializer(serializers.ModelSerializer): - class Meta: - model = Teacher - fields = ['course', 'id', 'is_admin', 'is_assistent'] diff --git a/backend/pigeonhole/apps/users/serializers.py b/backend/pigeonhole/apps/users/serializers.py new file mode 100644 index 00000000..2856028d --- /dev/null +++ b/backend/pigeonhole/apps/users/serializers.py @@ -0,0 +1,41 @@ +from django.contrib.auth.models import update_last_login +from rest_framework import serializers +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework_simplejwt.settings import api_settings + +from backend.pigeonhole.apps.users.models import User, Student, Teacher + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'e_mail', 'first_name', 'last_name'] + + +class StudentSerializer(serializers.ModelSerializer): + class Meta: + model = Student + fields = ['number', 'course', 'id'] + + +class TeacherSerializer(serializers.ModelSerializer): + class Meta: + model = Teacher + fields = ['course', 'id', 'is_admin', 'is_assistent'] + + +class LoginSerializer(TokenObtainPairSerializer): + + def validate(self, attrs): + data = super().validate(attrs) + + refresh = self.get_token(self.user) + + data['user'] = UserSerializer(self.user).data + data['refresh'] = str(refresh) + data['access'] = str(refresh.access_token) + + if api_settings.UPDATE_LAST_LOGIN: + update_last_login(None, self.user) + + return data diff --git a/backend/pigeonhole/apps/users/viewsets.py b/backend/pigeonhole/apps/users/viewsets.py new file mode 100644 index 00000000..cc6d5322 --- /dev/null +++ b/backend/pigeonhole/apps/users/viewsets.py @@ -0,0 +1,24 @@ +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework_simplejwt.exceptions import TokenError, InvalidToken +from rest_framework_simplejwt.views import TokenObtainPairView + +from backend.pigeonhole.apps.users.serializers import LoginSerializer + + +class LoginViewSet(ModelViewSet, TokenObtainPairView): + serializer_class = LoginSerializer + permission_classes = (AllowAny,) + http_method_names = ['post'] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + + return Response(serializer.validated_data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/settings.py b/backend/pigeonhole/settings.py index 7edfb930..5a1ef8a9 100644 --- a/backend/pigeonhole/settings.py +++ b/backend/pigeonhole/settings.py @@ -115,7 +115,12 @@ } REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'DEFAULT_PAGINATION_CLASS': ( + 'rest_framework.pagination.PageNumberPagination', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), 'PAGE_SIZE': 10 } From ea7ccd79b8c3b9d083d56c7d4817db90d5d65f53 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Sun, 10 Mar 2024 04:43:35 +0100 Subject: [PATCH 19/58] add auth route --- backend/pigeonhole/apps/users/urls.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/pigeonhole/apps/users/urls.py diff --git a/backend/pigeonhole/apps/users/urls.py b/backend/pigeonhole/apps/users/urls.py new file mode 100644 index 00000000..9a6b81ec --- /dev/null +++ b/backend/pigeonhole/apps/users/urls.py @@ -0,0 +1,8 @@ +from rest_framework.routers import SimpleRouter + +from backend.pigeonhole.apps.users.viewsets import LoginViewSet + +routes = SimpleRouter() + +# AUTHENTICATION +routes.register(r'auth/login', LoginViewSet, basename='auth-login') From fb9452fc9bd0066c45b4a58b7825abfb2bf6fdbe Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Sun, 10 Mar 2024 05:06:09 +0100 Subject: [PATCH 20/58] fix route --- backend/pigeonhole/apps/users/urls.py | 8 -------- backend/pigeonhole/urls.py | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 backend/pigeonhole/apps/users/urls.py diff --git a/backend/pigeonhole/apps/users/urls.py b/backend/pigeonhole/apps/users/urls.py deleted file mode 100644 index 9a6b81ec..00000000 --- a/backend/pigeonhole/apps/users/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.routers import SimpleRouter - -from backend.pigeonhole.apps.users.viewsets import LoginViewSet - -routes = SimpleRouter() - -# AUTHENTICATION -routes.register(r'auth/login', LoginViewSet, basename='auth-login') diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 2cb677dd..faaf23f0 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,11 +1,13 @@ from django.urls import include, path from rest_framework import routers +from backend.pigeonhole.apps.users.viewsets import LoginViewSet from backend.testapi import views router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'groups', views.GroupViewSet) +router.register(r'login', LoginViewSet, basename='auth-login') # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. From 54be0226af20d683b6ec41aa9f36b9f5c9b6aa21 Mon Sep 17 00:00:00 2001 From: gilles-arnout Date: Sun, 10 Mar 2024 13:00:21 +0100 Subject: [PATCH 21/58] fix pages structure --- frontend/pages/index.tsx | 15 -------- frontend/src/app/layout.tsx | 16 +++++++++ frontend/src/app/page.tsx | 44 ++++++------------------ frontend/src/components/ui/LoginCard.tsx | 17 --------- frontend/src/components/ui/LoginForm.tsx | 1 + 5 files changed, 27 insertions(+), 66 deletions(-) delete mode 100644 frontend/pages/index.tsx create mode 100644 frontend/src/app/layout.tsx delete mode 100644 frontend/src/components/ui/LoginCard.tsx diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx deleted file mode 100644 index 55d46771..00000000 --- a/frontend/pages/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// pages/index.tsx -import React from 'react'; -import LoginCard from '@/components/ui/LoginCard'; -import LoginForm from '@/components/ui/LoginForm'; - - -const Home = () => { - return ( -
- -
- ); -}; - -export default Home; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 00000000..a14e64fc --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ee07830a..b7f71cda 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,37 +1,13 @@ -"use client" +import React from 'react'; +import LoginForm from '@/components/ui/LoginForm'; -import Image from "next/image"; -import React, { useState, useEffect } from 'react'; - -export default function Home() { - - - const [data, setData] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('http://127.0.0.1:8000/groups/'); - if (response.ok) { - const json = await response.json(); - setData(json); - } else { - console.error('Failed to fetch data'); - } - } catch (error) { - console.error('Error:', error); - } - }; - - fetchData(); - }, []); - - - return ( -
-

Deployment test:

-

{JSON.stringify(data)}

-
- ); +const Login = () => { + return ( +
+ +
+ ) } + +export default Login diff --git a/frontend/src/components/ui/LoginCard.tsx b/frontend/src/components/ui/LoginCard.tsx deleted file mode 100644 index 541689d5..00000000 --- a/frontend/src/components/ui/LoginCard.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import Button from '@mui/material/Button'; -import Stack from '@mui/material/Stack'; - -export default function ContainedButtons() { - return ( - - - - - - ); -} \ No newline at end of file diff --git a/frontend/src/components/ui/LoginForm.tsx b/frontend/src/components/ui/LoginForm.tsx index b84149bf..49008083 100644 --- a/frontend/src/components/ui/LoginForm.tsx +++ b/frontend/src/components/ui/LoginForm.tsx @@ -1,3 +1,4 @@ +"use client" import React, {useState} from 'react'; import {Box, Button, Container, CssBaseline, TextField, Typography} from '@mui/material'; import {createTheme, ThemeProvider} from '@mui/material/styles'; From 0ddd30cd0779ec19cddeb60f45e3af463dc25f9c Mon Sep 17 00:00:00 2001 From: rdyselinck Date: Sun, 10 Mar 2024 14:47:29 +0100 Subject: [PATCH 22/58] GET requests support for groups+submissions, but error for groups when needing deadline --- backend/pigeonhole/apps/groups/models.py | 2 +- backend/pigeonhole/apps/groups/views.py | 17 +++++++++++++++++ backend/pigeonhole/apps/submissions/models.py | 2 +- backend/pigeonhole/apps/submissions/views.py | 17 +++++++++++++++++ backend/pigeonhole/urls.py | 5 ++++- 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 backend/pigeonhole/apps/groups/views.py create mode 100644 backend/pigeonhole/apps/submissions/views.py diff --git a/backend/pigeonhole/apps/groups/models.py b/backend/pigeonhole/apps/groups/models.py index 2c8d67b1..cf9bab6e 100644 --- a/backend/pigeonhole/apps/groups/models.py +++ b/backend/pigeonhole/apps/groups/models.py @@ -19,4 +19,4 @@ class Group(models.Model): class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group - fields = ["group_id", "group_nr", "final_score", "project_id", "student"] + fields = ["group_id", "group_nr", "final_score", "project_id", "student", "feedback", "project_id"] diff --git a/backend/pigeonhole/apps/groups/views.py b/backend/pigeonhole/apps/groups/views.py new file mode 100644 index 00000000..fe5473ec --- /dev/null +++ b/backend/pigeonhole/apps/groups/views.py @@ -0,0 +1,17 @@ +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from backend.pigeonhole.apps.groups.models import Group, GroupSerializer + + +class GroupViewSet(viewsets.ModelViewSet): + queryset = Group.objects.all() + serializer_class = GroupSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def list(self, request, *args, **kwargs): + serializer = GroupSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/apps/submissions/models.py b/backend/pigeonhole/apps/submissions/models.py index da12e031..2ac9ead2 100644 --- a/backend/pigeonhole/apps/submissions/models.py +++ b/backend/pigeonhole/apps/submissions/models.py @@ -21,4 +21,4 @@ class Submissions(models.Model): class SubmissionsSerializer(serializers.ModelSerializer): class Meta: model = Submissions - fields = ['submission_id', 'group_id', 'file', 'timestamp', 'submission_nr'] + fields = ['submission_id', 'group_id', 'file', 'timestamp', 'submission_nr', 'output_test'] diff --git a/backend/pigeonhole/apps/submissions/views.py b/backend/pigeonhole/apps/submissions/views.py new file mode 100644 index 00000000..7079c1d5 --- /dev/null +++ b/backend/pigeonhole/apps/submissions/views.py @@ -0,0 +1,17 @@ +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from backend.pigeonhole.apps.submissions.models import Submissions, SubmissionsSerializer + + +class SubmissionsViewset(viewsets.ModelViewSet): + queryset = Submissions.objects.all() + serializer_class = SubmissionsSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def list(self, request, *args, **kwargs): + serializer = SubmissionsSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 7a860619..c965476d 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -5,6 +5,8 @@ from backend.testapi import views as test_views from backend.pigeonhole.apps.courses.views import CourseViewSet from backend.pigeonhole.apps.projects.views import ProjectViewSet +from backend.pigeonhole.apps.submissions.views import SubmissionsViewset +from backend.pigeonhole.apps.groups.views import GroupViewSet #from backend.pigeonhole.apps.projects import views as project_views from drf_yasg.views import get_schema_view from drf_yasg import openapi @@ -24,9 +26,10 @@ router = routers.DefaultRouter() router.register(r'users', test_views.UserViewSet) -router.register(r'groups', test_views.GroupViewSet) +router.register(r'groups', GroupViewSet) router.register(r'courses', CourseViewSet) router.register(r'courses//projects/', ProjectViewSet) +router.register(r'submissions', SubmissionsViewset) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. From b14d75bf5623bfb4f94109edc52cf586ff3e525a Mon Sep 17 00:00:00 2001 From: rdyselinck Date: Sun, 10 Mar 2024 16:56:40 +0100 Subject: [PATCH 23/58] removed deadline temporary to find bug --- backend/pigeonhole/apps/projects/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/pigeonhole/apps/projects/models.py b/backend/pigeonhole/apps/projects/models.py index f651f5bd..d998a3a9 100644 --- a/backend/pigeonhole/apps/projects/models.py +++ b/backend/pigeonhole/apps/projects/models.py @@ -11,21 +11,20 @@ class Project(models.Model): course_id = models.ForeignKey(Course, on_delete=models.CASCADE) name = models.CharField(max_length=256) description = models.TextField() - deadline = models.DateTimeField() + # deadline = models.DateTimeField() visible = models.BooleanField(default=False) class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ['project_id', 'course_id', 'name', 'description', 'deadline', 'visible'] + fields = ['project_id', 'course_id', 'name', 'description', 'visible'] class Conditions(models.Model): condition_id = models.BigAutoField(primary_key=True) submission_id = models.ForeignKey(Project, on_delete=models.CASCADE) condition = models.CharField(max_length=256) - deadline = models.DateTimeField() test_file_location = models.CharField(max_length=512, null=True) test_file_type = models.CharField(max_length=256, null=True) From ca408dd4990ba9c160b34d623c4c6f600558c4d9 Mon Sep 17 00:00:00 2001 From: rdyselinck Date: Sun, 10 Mar 2024 17:01:45 +0100 Subject: [PATCH 24/58] without deadline in project seems to work --- .../apps/courses/migrations/0001_initial.py | 3 +- .../apps/groups/migrations/0001_initial.py | 3 +- .../apps/groups/migrations/0002_initial.py | 3 +- .../apps/projects/migrations/0001_initial.py | 13 +++--- backend/pigeonhole/apps/projects/models.py | 10 ++--- .../submissions/migrations/0001_initial.py | 11 ++--- ...ions_file_alter_submissions_output_test.py | 26 ------------ .../apps/users/migrations/0001_initial.py | 40 +++++-------------- 8 files changed, 31 insertions(+), 78 deletions(-) delete mode 100644 backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py diff --git a/backend/pigeonhole/apps/courses/migrations/0001_initial.py b/backend/pigeonhole/apps/courses/migrations/0001_initial.py index 9d548232..48a29d53 100644 --- a/backend/pigeonhole/apps/courses/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/courses/migrations/0001_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-10 16:00 from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ diff --git a/backend/pigeonhole/apps/groups/migrations/0001_initial.py b/backend/pigeonhole/apps/groups/migrations/0001_initial.py index fc8925aa..1102b309 100644 --- a/backend/pigeonhole/apps/groups/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/groups/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-10 16:00 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ diff --git a/backend/pigeonhole/apps/groups/migrations/0002_initial.py b/backend/pigeonhole/apps/groups/migrations/0002_initial.py index 354b0401..649b27b5 100644 --- a/backend/pigeonhole/apps/groups/migrations/0002_initial.py +++ b/backend/pigeonhole/apps/groups/migrations/0002_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-10 16:00 from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ diff --git a/backend/pigeonhole/apps/projects/migrations/0001_initial.py b/backend/pigeonhole/apps/projects/migrations/0001_initial.py index c4390c94..46f86d20 100644 --- a/backend/pigeonhole/apps/projects/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/projects/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-10 16:00 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -26,7 +27,7 @@ class Migration(migrations.Migration): name='ForbiddenExtension', fields=[ ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), - ('extension', models.IntegerField()), + ('extension', models.CharField(max_length=512)), ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), @@ -34,19 +35,17 @@ class Migration(migrations.Migration): name='Conditions', fields=[ ('condition_id', models.BigAutoField(primary_key=True, serialize=False)), - ('condition', models.CharField(max_length=256)), - ('deadline', models.DateTimeField()), + ('condition', models.TextField(max_length=256)), ('test_file_location', models.CharField(max_length=512, null=True)), ('test_file_type', models.CharField(max_length=256, null=True)), - ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - to='projects.project')), + ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), migrations.CreateModel( name='AllowedExtension', fields=[ ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), - ('extension', models.IntegerField()), + ('extension', models.CharField(max_length=512)), ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), ], ), diff --git a/backend/pigeonhole/apps/projects/models.py b/backend/pigeonhole/apps/projects/models.py index d998a3a9..fbc15548 100644 --- a/backend/pigeonhole/apps/projects/models.py +++ b/backend/pigeonhole/apps/projects/models.py @@ -24,7 +24,7 @@ class Meta: class Conditions(models.Model): condition_id = models.BigAutoField(primary_key=True) submission_id = models.ForeignKey(Project, on_delete=models.CASCADE) - condition = models.CharField(max_length=256) + condition = models.TextField(max_length=256) test_file_location = models.CharField(max_length=512, null=True) test_file_type = models.CharField(max_length=256, null=True) @@ -32,17 +32,17 @@ class Conditions(models.Model): @property def get_forbidden_extensions(self): - return ForbiddenExtension.objects.filter(project_id=self.submission_id) + return ForbiddenExtension.objects.filter(project_id=self.project_id) @property def get_allowed_extensions(self): - return AllowedExtension.objects.filter(project_id=self.submission_id) + return AllowedExtension.objects.filter(project_id=self.project_id) class AllowedExtension(models.Model): extension_id = models.BigAutoField(primary_key=True) project_id = models.ForeignKey(Project, on_delete=models.CASCADE) - extension = models.IntegerField() + extension = models.CharField(max_length=512) objects = models.Manager() @@ -50,6 +50,6 @@ class AllowedExtension(models.Model): class ForbiddenExtension(models.Model): extension_id = models.BigAutoField(primary_key=True) project_id = models.ForeignKey(Project, on_delete=models.CASCADE) - extension = models.IntegerField() + extension = models.CharField(max_length=512) objects = models.Manager() diff --git a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py index f393bd58..8c4d718c 100644 --- a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-10 16:00 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -17,13 +18,9 @@ class Migration(migrations.Migration): fields=[ ('submission_id', models.BigAutoField(primary_key=True, serialize=False)), ('submission_nr', models.IntegerField()), - ('file', models.FileField( - upload_to='uploads///')), + ('file', models.FileField(null=True, upload_to='uploads///')), ('timestamp', models.DateTimeField(auto_now_add=True)), - ('output_test', models.FileField( - upload_to='uploads///output_test/')), + ('output_test', models.FileField(null=True, upload_to='uploads///output_test/')), ('group_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='groups.group')), ], ), diff --git a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index 56752ac0..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('submissions', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField(null=True, - upload_to='uploads///'), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField(null=True, - upload_to='uploads///output_test/'), - ), - ] diff --git a/backend/pigeonhole/apps/users/migrations/0001_initial.py b/backend/pigeonhole/apps/users/migrations/0001_initial.py index 42953dcd..016b1bb0 100644 --- a/backend/pigeonhole/apps/users/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:03 +# Generated by Django 5.0.3 on 2024-03-10 16:00 import django.contrib.auth.models import django.contrib.auth.validators @@ -9,6 +9,7 @@ class Migration(migrations.Migration): + initial = True dependencies = [ @@ -23,35 +24,16 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, - help_text='Designates that this user has all permissions ' - 'without explicitly assigning them.', - verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, - help_text='Required. 150 characters or fewer. Letters, digits ' - 'and @/./+/-/_ only.', - max_length=150, unique=True, - validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], - verbose_name='username')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, - help_text='Designates whether the user can log into this admin site.', - verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, - help_text='Designates whether this user should be treated as active. ' - 'Unselect this instead of deleting accounts.', - verbose_name='active')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, - help_text='The groups this user belongs to. A user will get all ' - 'permissions granted to each of their groups.', - related_name='user_set', related_query_name='user', to='auth.group', - verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', - related_name='user_set', related_query_name='user', - to='auth.permission', verbose_name='user permissions')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'user', @@ -66,8 +48,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Student', fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, - to=settings.AUTH_USER_MODEL)), + ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), ('number', models.IntegerField()), ('course', models.ManyToManyField(to='courses.course')), ], @@ -75,8 +56,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Teacher', fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, - to=settings.AUTH_USER_MODEL)), + ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), ('is_admin', models.BooleanField(default=False)), ('is_assistent', models.BooleanField(default=False)), ('course', models.ManyToManyField(to='courses.course')), From 2f9a381e990c4293284e68c913c398a3250bbc52 Mon Sep 17 00:00:00 2001 From: rdyselinck Date: Sun, 10 Mar 2024 17:12:08 +0100 Subject: [PATCH 25/58] deadline for projects fixed --- .../migrations/0002_project_deadline.py | 18 ++++++++++++++++++ .../migrations/0003_alter_project_deadline.py | 18 ++++++++++++++++++ .../migrations/0004_alter_project_deadline.py | 18 ++++++++++++++++++ backend/pigeonhole/apps/projects/models.py | 4 ++-- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py create mode 100644 backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py create mode 100644 backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py diff --git a/backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py b/backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py new file mode 100644 index 00000000..330da245 --- /dev/null +++ b/backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-03-10 16:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='deadline', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py b/backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py new file mode 100644 index 00000000..639d1bc8 --- /dev/null +++ b/backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-03-10 16:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0002_project_deadline'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='deadline', + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py b/backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py new file mode 100644 index 00000000..f9b12a86 --- /dev/null +++ b/backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-03-10 16:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0003_alter_project_deadline'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='deadline', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/backend/pigeonhole/apps/projects/models.py b/backend/pigeonhole/apps/projects/models.py index fbc15548..8834cf12 100644 --- a/backend/pigeonhole/apps/projects/models.py +++ b/backend/pigeonhole/apps/projects/models.py @@ -11,14 +11,14 @@ class Project(models.Model): course_id = models.ForeignKey(Course, on_delete=models.CASCADE) name = models.CharField(max_length=256) description = models.TextField() - # deadline = models.DateTimeField() + deadline = models.DateTimeField(null=True, blank=True) visible = models.BooleanField(default=False) class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ['project_id', 'course_id', 'name', 'description', 'visible'] + fields = ['project_id', 'course_id', 'name', 'description', 'visible', 'deadline'] class Conditions(models.Model): From 82c29e2310d3ba1f29065f7dff85e9a382d28f32 Mon Sep 17 00:00:00 2001 From: rdyselinck Date: Sun, 10 Mar 2024 18:57:58 +0100 Subject: [PATCH 26/58] Everything fixed to create submissions --- backend/pigeonhole/apps/groups/models.py | 2 +- backend/pigeonhole/apps/groups/views.py | 9 ++++ backend/pigeonhole/apps/projects/views.py | 3 ++ backend/pigeonhole/apps/submissions/views.py | 9 ++++ backend/pigeonhole/apps/users/models.py | 3 +- backend/pigeonhole/apps/users/views.py | 48 ++++++++++++++++++++ backend/pigeonhole/urls.py | 4 +- 7 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 backend/pigeonhole/apps/users/views.py diff --git a/backend/pigeonhole/apps/groups/models.py b/backend/pigeonhole/apps/groups/models.py index cf9bab6e..c806f3c9 100644 --- a/backend/pigeonhole/apps/groups/models.py +++ b/backend/pigeonhole/apps/groups/models.py @@ -19,4 +19,4 @@ class Group(models.Model): class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group - fields = ["group_id", "group_nr", "final_score", "project_id", "student", "feedback", "project_id"] + fields = ["group_id", "group_nr", "final_score", "project_id", "student", "feedback"] diff --git a/backend/pigeonhole/apps/groups/views.py b/backend/pigeonhole/apps/groups/views.py index fe5473ec..23bbeca7 100644 --- a/backend/pigeonhole/apps/groups/views.py +++ b/backend/pigeonhole/apps/groups/views.py @@ -1,6 +1,7 @@ from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response + from backend.pigeonhole.apps.groups.models import Group, GroupSerializer @@ -15,3 +16,11 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): serializer = GroupSerializer(self.queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + + def create(self, request, *args, **kwargs): + serializer = GroupSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index 692c2cfa..e5db62ff 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -12,6 +12,9 @@ class ProjectViewSet(viewsets.ModelViewSet): serializer_class = ProjectSerializer permission_classes = [IsAuthenticated & CanAccessProject] + def perform_create(self, serializer): + serializer.save(user=self.request.user) + def create(self, request, *args, **kwargs): serializer = ProjectSerializer(data=request.data) if serializer.is_valid(): diff --git a/backend/pigeonhole/apps/submissions/views.py b/backend/pigeonhole/apps/submissions/views.py index 7079c1d5..8a03e474 100644 --- a/backend/pigeonhole/apps/submissions/views.py +++ b/backend/pigeonhole/apps/submissions/views.py @@ -1,6 +1,7 @@ from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response + from backend.pigeonhole.apps.submissions.models import Submissions, SubmissionsSerializer @@ -15,3 +16,11 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): serializer = SubmissionsSerializer(self.queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + + def create(self, request, *args, **kwargs): + serializer = SubmissionsSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index ce1c243d..4a35e705 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -17,7 +17,7 @@ def name(self): class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'e_mail', 'first_name', 'last_name'] + fields = ['id', 'email', 'first_name', 'last_name'] class Student(models.Model): @@ -29,6 +29,7 @@ class Student(models.Model): class StudentSerializer(serializers.ModelSerializer): + id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) class Meta: model = Student fields = ['number', 'course', 'id'] diff --git a/backend/pigeonhole/apps/users/views.py b/backend/pigeonhole/apps/users/views.py new file mode 100644 index 00000000..0af8843f --- /dev/null +++ b/backend/pigeonhole/apps/users/views.py @@ -0,0 +1,48 @@ +from rest_framework import viewsets, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from backend.pigeonhole.apps.users.models import Student, StudentSerializer, User, UserSerializer + + +class StudentViewSet(viewsets.ModelViewSet): + queryset = Student.objects.all() + serializer_class = StudentSerializer + permission_classes = [IsAuthenticated] + + def create(self, request, *args, **kwargs): + serializer = StudentSerializer(data=request.data) + print(serializer) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def list(self, request, *args, **kwargs): + serializer = StudentSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def list(self, request, *args, **kwargs): + serializer = UserSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def create(self, request, *args, **kwargs): + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index c965476d..3676ea4a 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -7,6 +7,7 @@ from backend.pigeonhole.apps.projects.views import ProjectViewSet from backend.pigeonhole.apps.submissions.views import SubmissionsViewset from backend.pigeonhole.apps.groups.views import GroupViewSet +from backend.pigeonhole.apps.users.views import StudentViewSet, UserViewSet #from backend.pigeonhole.apps.projects import views as project_views from drf_yasg.views import get_schema_view from drf_yasg import openapi @@ -25,7 +26,8 @@ ) router = routers.DefaultRouter() -router.register(r'users', test_views.UserViewSet) +router.register(r'users', UserViewSet) +router.register(r'students', StudentViewSet) router.register(r'groups', GroupViewSet) router.register(r'courses', CourseViewSet) router.register(r'courses//projects/', ProjectViewSet) From c5fcf3ee720b1074bb60ca851245ce9eec940002 Mon Sep 17 00:00:00 2001 From: avoyen Date: Sun, 10 Mar 2024 19:37:13 +0100 Subject: [PATCH 27/58] API works now, fixed URLs and the requests --- backend/pigeonhole/apps/projects/views.py | 28 ++++++++++++++++------- backend/pigeonhole/urls.py | 3 +-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index e5db62ff..c5f18bbe 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -15,7 +15,16 @@ class ProjectViewSet(viewsets.ModelViewSet): def perform_create(self, serializer): serializer.save(user=self.request.user) + def list(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('pk') + + serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) + return Response(serializer.data, status=status.HTTP_200_OK) + def create(self, request, *args, **kwargs): + print("ceating new project") + course_id = kwargs.get('course_id') serializer = ProjectSerializer(data=request.data) if serializer.is_valid(): serializer.save() @@ -26,9 +35,10 @@ def create(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') + project = Project.objects.get(pk=project_id) project.delete() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response({"message": "Project has been deleted successfully."}, status=status.HTTP_204_NO_CONTENT) def retrieve(self, request, *args, **kwargs): course_id = kwargs.get('course_id') @@ -40,14 +50,16 @@ def retrieve(self, request, *args, **kwargs): def update(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') + project = Project.objects.get(pk=project_id) serializer = ProjectSerializer(project, data=request.data) - serializer.save() + if serializer.is_valid(): + serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) - def archive(self, request, *args, **kwargs): - # Not sure what to do here yet - course_id = kwargs.get('course_id') - project_id = kwargs.get('pk') - serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) - return Response(serializer, status=status.HTTP_200_OK) + def partial_update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 3676ea4a..50d52d06 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -30,8 +30,8 @@ router.register(r'students', StudentViewSet) router.register(r'groups', GroupViewSet) router.register(r'courses', CourseViewSet) -router.register(r'courses//projects/', ProjectViewSet) router.register(r'submissions', SubmissionsViewset) +router.register(r'courses/(?P[^/.]+)/projects', ProjectViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. @@ -41,7 +41,6 @@ path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), path("admin/", admin.site.urls), - path('courses//projects/', ProjectViewSet.as_view({'post': 'create'}), name='project-create'), ] urlpatterns += router.urls From 8cbc51fad012604bd22aaafa7842575b7c38e3a6 Mon Sep 17 00:00:00 2001 From: avoyen Date: Sun, 10 Mar 2024 20:22:35 +0100 Subject: [PATCH 28/58] list fixed --- backend/pigeonhole/apps/projects/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index c5f18bbe..f748799d 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -17,9 +17,7 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): course_id = kwargs.get('course_id') - project_id = kwargs.get('pk') - - serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) + serializer = ProjectSerializer(Project.objects.filter(course_id=course_id), many=True) return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, *args, **kwargs): From ec129d7f5eb24ca380b467a6c4fb9f0e8cf44a70 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Sun, 10 Mar 2024 22:06:46 +0100 Subject: [PATCH 29/58] fix merge --- ...ions_file_alter_submissions_output_test.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py diff --git a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py new file mode 100644 index 00000000..483e5352 --- /dev/null +++ b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.2 on 2024-03-02 21:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('submissions', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='submissions', + name='file', + field=models.FileField(null=True, + upload_to='uploads///'), + ), + migrations.AlterField( + model_name='submissions', + name='output_test', + field=models.FileField(null=True, + upload_to='uploads///output_test/'), + ), + ] \ No newline at end of file From cefeb9874314d9a2c1afb71cda5398df614f1c43 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Sun, 10 Mar 2024 23:04:22 +0100 Subject: [PATCH 30/58] course api tests, permission fixes, test fixes --- .../pigeonhole/apps/courses/permissions.py | 29 +-- .../tests/test_models/test_conditions.py | 2 +- .../tests/test_models/test_groups.py | 1 + .../tests/test_models/test_project.py | 1 + .../tests/test_models/test_submissions.py | 1 + .../pigeonhole/tests/test_views/__init__.py | 0 .../tests/test_views/test_course.py | 173 ++++++++++++++++++ 7 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 backend/pigeonhole/tests/test_views/__init__.py create mode 100644 backend/pigeonhole/tests/test_views/test_course.py diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py index 201b666e..9e456045 100644 --- a/backend/pigeonhole/apps/courses/permissions.py +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -1,30 +1,19 @@ from rest_framework import permissions -from backend.pigeonhole.apps.users.models import Student, Teacher +from backend.pigeonhole.apps.users.models import Teacher, Student class CourseUserPermissions(permissions.BasePermission): def has_permission(self, request, view): if request.user.is_superuser: return True - if isinstance(request.user, Teacher): - return True - - if isinstance(request.user, Student): - return view.action in ['list', 'retrieve'] - - return False - - def has_object_permission(self, request, view, obj): - if request.user.is_superuser: - return True - if isinstance(request.user, Teacher): - if request.user.is_admin: + if Teacher.objects.filter(id=request.user.id).exists(): + teacher = Teacher.objects.get(id=request.user.id) + if teacher.is_admin: return True - elif Teacher.objects.filter(id=request.user.id, course=obj).exists(): + # Check if the teacher is assigned to the course + course = view.kwargs.get('pk') + if teacher.course.filter(course_id=course).exists(): return True + return view.action in ['list', 'retrieve', 'create'] + elif Student.objects.filter(id=request.user.id).exists(): return view.action in ['list', 'retrieve'] - - if isinstance(request.user, Student): - return view.action in ['list', 'retrieve'] - - return False diff --git a/backend/pigeonhole/tests/test_models/test_conditions.py b/backend/pigeonhole/tests/test_models/test_conditions.py index 37307ab7..b6748042 100644 --- a/backend/pigeonhole/tests/test_models/test_conditions.py +++ b/backend/pigeonhole/tests/test_models/test_conditions.py @@ -34,6 +34,7 @@ def setUp(self): project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description" ) @@ -41,7 +42,6 @@ def setUp(self): self.conditions = Conditions.objects.create( submission_id=project, condition="Condition 1", - deadline="2021-12-12 12:12:12", test_file_location="path/to/test", test_file_type="txt" ) diff --git a/backend/pigeonhole/tests/test_models/test_groups.py b/backend/pigeonhole/tests/test_models/test_groups.py index 0ece53b9..b7d52642 100644 --- a/backend/pigeonhole/tests/test_models/test_groups.py +++ b/backend/pigeonhole/tests/test_models/test_groups.py @@ -47,6 +47,7 @@ def setUp(self): project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description", ) diff --git a/backend/pigeonhole/tests/test_models/test_project.py b/backend/pigeonhole/tests/test_models/test_project.py index 11285c91..4f66e0f1 100644 --- a/backend/pigeonhole/tests/test_models/test_project.py +++ b/backend/pigeonhole/tests/test_models/test_project.py @@ -34,6 +34,7 @@ def setUp(self): self.project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description", ) diff --git a/backend/pigeonhole/tests/test_models/test_submissions.py b/backend/pigeonhole/tests/test_models/test_submissions.py index c6efed08..3dc59031 100644 --- a/backend/pigeonhole/tests/test_models/test_submissions.py +++ b/backend/pigeonhole/tests/test_models/test_submissions.py @@ -37,6 +37,7 @@ def setUp(self): project = Project.objects.create( name="Project", course_id=course, + deadline="2021-12-12 12:12:12", description="Project Description", ) diff --git a/backend/pigeonhole/tests/test_views/__init__.py b/backend/pigeonhole/tests/test_views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_course.py b/backend/pigeonhole/tests/test_views/test_course.py new file mode 100644 index 00000000..5c3f8bd6 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course.py @@ -0,0 +1,173 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from rest_framework import status +from backend.pigeonhole.apps.users.models import User, Teacher, Student +from backend.pigeonhole.apps.courses.models import Course + +API_ENDPOINT = '/courses/' # Updated the API_ENDPOINT + + +class CourseTestTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", description="This is not of the teacher") + + # Create a regular user (teacher) + self.user = User.objects.create_user( + username="teacher_username", + email="teacher@gmail.com", + first_name="Kermit", + last_name="The Frog", + ) + + # Create a Teacher instance and use .set() to assign the course + self.teacher = Teacher.objects.create(id=self.user) + self.teacher.course.set([self.course]) + + # Authenticate the test client with the teacher user + self.client.force_authenticate(user=self.user) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 3) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course.refresh_from_db() + self.assertEqual(self.course.name, updated_data['name']) + self.assertEqual(self.course.description, updated_data['description']) + + # TODO + def test_update_course_not_of_teacher(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + # TODO + def test_delete_course_not_of_teacher(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_retrieve_course(self): + response = self.client.get(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['name'], self.course.name) + self.assertEqual(response.data['description'], self.course.description) + + +class CourseTestStudent(TestCase): + def setUp(self): + self.client = APIClient() + + # Create a regular user (teacher) + self.user = User.objects.create_user( + username="teacher_username", + email="kermit@gmail.com", + first_name="Kermit", + last_name="The Frog" + ) + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + # Provide a value for the "number" field when creating the Student instance + self.student = Student.objects.create(id=self.user, number=123456) + self.student.course.set([self.course]) + + # Authenticate the test client with the regular user + self.client.force_authenticate(user=self.user) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.course.refresh_from_db() + self.assertNotEqual(self.course.name, updated_data['name']) + self.assertNotEqual(self.course.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + +class CourseTestUnauthorized(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.course.refresh_from_db() + self.assertNotEqual(self.course.name, updated_data['name']) + self.assertNotEqual(self.course.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(len(response.data), 1) \ No newline at end of file From a220d0311e69aea489b36c271a0c2fc54c3ca8d3 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Mon, 11 Mar 2024 04:02:37 +0100 Subject: [PATCH 31/58] add basic user admin --- backend/pigeonhole/apps/users/admin.py | 22 ++++++++++++++++++++++ backend/pigeonhole/urls.py | 4 +++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 backend/pigeonhole/apps/users/admin.py diff --git a/backend/pigeonhole/apps/users/admin.py b/backend/pigeonhole/apps/users/admin.py new file mode 100644 index 00000000..b674d1fc --- /dev/null +++ b/backend/pigeonhole/apps/users/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin + +from backend.pigeonhole.apps.users.models import User + + +class UserAdmin(BaseUserAdmin): + list_display = ('email',) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () + fieldsets = ( + (None, {'fields': ( + 'email', + 'password', + 'first_name', + 'last_name', + )}), + ) + + +admin.site.register(User, UserAdmin) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index faaf23f0..883e1350 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,3 +1,4 @@ +from django.contrib import admin from django.urls import include, path from rest_framework import routers @@ -13,7 +14,8 @@ # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path('admin/', admin.site.urls) ] urlpatterns += router.urls From c3dd5a47fc4c97ce7ae3481f78c06e98307d0661 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Mon, 11 Mar 2024 10:25:20 +0100 Subject: [PATCH 32/58] partial projects tests, todo invalid project & unautherised --- .../pigeonhole/apps/projects/permissions.py | 24 +- backend/pigeonhole/apps/projects/views.py | 14 +- .../tests/test_views/test_course.py | 86 +++- .../tests/test_views/test_project.py | 478 ++++++++++++++++++ 4 files changed, 584 insertions(+), 18 deletions(-) create mode 100644 backend/pigeonhole/tests/test_views/test_project.py diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index d08a56b5..49a292a2 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -3,21 +3,25 @@ class CanAccessProject(permissions.BasePermission): - # Custom permission class to determine if the currect user has access - # to the project data. def has_permission(self, request, view): user = request.user subject_id = view.kwargs.get('course_id') - # If the user is a teacher, grant access. - if isinstance(user, Teacher): - if user.course.filter(id=subject_id).exists(): + + if Teacher.objects.filter(id=user.id).exists(): + teacher = Teacher.objects.get(id=user.id) + + # Check if the teacher is assigned to the specified course + if teacher.course.filter(course_id=subject_id).exists(): return True - elif isinstance(user, Teacher) and user.is_admin: - return True - # If the user is a student, grant access only to their own projects. - elif isinstance(user, Student): - if user.course.filter(id=subject_id).exists(): + elif teacher.is_admin: return True + return view.action in ['list', 'retrieve', 'create'] and teacher.course.filter( + course_id=subject_id).exists() + # If the user is a student, grant access only to their own projects. + elif Student.objects.filter(id=user.id).exists(): + student = Student.objects.get(id=user.id) + if student.course.filter(course_id=subject_id).exists(): + return view.action in ['list', 'retrieve'] elif request.user.is_superuser: return True return False diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index f748799d..ba735ab4 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from .models import Project, ProjectSerializer +from .models import Project, ProjectSerializer, Course from .permissions import CanAccessProject @@ -18,6 +18,9 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): course_id = kwargs.get('course_id') serializer = ProjectSerializer(Project.objects.filter(course_id=course_id), many=True) + # Check whether the course exists + if not Course.objects.filter(course_id=course_id).exists(): + return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, *args, **kwargs): @@ -41,8 +44,17 @@ def destroy(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') + + # Check whether the course exists + if not Course.objects.filter(course_id=course_id).exists(): + return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + + # Check whether the project exists + if not Project.objects.filter(pk=project_id).exists(): + return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) + return Response(serializer.data, status=status.HTTP_200_OK) def update(self, request, *args, **kwargs): diff --git a/backend/pigeonhole/tests/test_views/test_course.py b/backend/pigeonhole/tests/test_views/test_course.py index 5c3f8bd6..984f508a 100644 --- a/backend/pigeonhole/tests/test_views/test_course.py +++ b/backend/pigeonhole/tests/test_views/test_course.py @@ -7,6 +7,78 @@ API_ENDPOINT = '/courses/' # Updated the API_ENDPOINT +class CourseTestAdminTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", + description="This is not of the teacher") + + self.course = Course.objects.create(**self.course_data) + + # Create a regular user (teacher) + self.user = User.objects.create_user( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + ) + + # Create a Teacher instance and use .set() to assign the course + self.teacher = Teacher.objects.create(id=self.user, is_admin=True) + self.teacher.course.set([self.course]) + + self.client.force_authenticate(user=self.user) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 3) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course.refresh_from_db() + self.assertEqual(self.course.name, updated_data['name']) + self.assertEqual(self.course.description, updated_data['description']) + + def test_update_course_not_of_teacher(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, + format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course_not_of_teacher.refresh_from_db() + self.assertEqual(self.course_not_of_teacher.name, updated_data['name']) + self.assertEqual(self.course_not_of_teacher.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_delete_course_not_of_teacher(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), Course.objects.count()) + + class CourseTestTeacher(TestCase): def setUp(self): self.client = APIClient() @@ -18,7 +90,8 @@ def setUp(self): self.course = Course.objects.create(**self.course_data) - self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", description="This is not of the teacher") + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", + description="This is not of the teacher") # Create a regular user (teacher) self.user = User.objects.create_user( @@ -51,13 +124,13 @@ def test_update_course(self): self.assertEqual(self.course.name, updated_data['name']) self.assertEqual(self.course.description, updated_data['description']) - # TODO def test_update_course_not_of_teacher(self): updated_data = { 'name': 'Updated Course', 'description': 'This course has been updated.' } - response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, format='json') + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, + format='json') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_delete_course(self): @@ -65,7 +138,6 @@ def test_delete_course(self): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Course.objects.count(), 1) - # TODO def test_delete_course_not_of_teacher(self): response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -73,7 +145,7 @@ def test_delete_course_not_of_teacher(self): def test_list_courses(self): response = self.client.get(API_ENDPOINT) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) + self.assertEqual(len(response.data), Course.objects.count()) def test_retrieve_course(self): response = self.client.get(f'{API_ENDPOINT}{self.course.course_id}/') @@ -132,7 +204,7 @@ def test_delete_course(self): def test_list_courses(self): response = self.client.get(API_ENDPOINT) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) + self.assertEqual(len(response.data), 2) class CourseTestUnauthorized(TestCase): @@ -170,4 +242,4 @@ def test_delete_course(self): def test_list_courses(self): response = self.client.get(API_ENDPOINT) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(len(response.data), 1) \ No newline at end of file + self.assertEqual(len(response.data), Course.objects.count()) diff --git a/backend/pigeonhole/tests/test_views/test_project.py b/backend/pigeonhole/tests/test_views/test_project.py new file mode 100644 index 00000000..2fad0a71 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project.py @@ -0,0 +1,478 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from rest_framework import status +from backend.pigeonhole.apps.users.models import User, Teacher, Student +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project + +API_ENDPOINT = '/courses/' # Updated the API_ENDPOINT + + +class ProjectTestAdminTeacher(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + ) + + self.teacher = Teacher.objects.create( + id=self.user, + is_admin=True + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.teacher.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.user) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Project.objects.count(), 2) + self.assertEqual(Project.objects.get(project_id=2).name, "Test Project 2") + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Project.objects.count(), 0) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + # tests with an invalid course + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + + +class ProjectTestTeacher(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + ) + + self.teacher = Teacher.objects.create( + id=self.user, + is_admin=False + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.course_not_of_teacher = Course.objects.create( + name="Test Course 2", + ) + + self.teacher.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.user) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Updated assertion to use the correct project_id from the response + created_project_id = response.data.get('project_id') + self.assertEqual(Project.objects.get(project_id=created_project_id).name, "Test Project 2") + self.assertEqual(Project.objects.count(), 2) + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Project.objects.count(), 0) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with a course not of the teacher + + def test_create_project_course_not_of_teacher(self): + response = self.client.post( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course_not_of_teacher.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_course_not_of_teacher(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_course_not_of_teacher(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + +class ProjectTestStudent(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username="student_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + ) + + self.student = Student.objects.create( + id=self.user, + number=1234567890 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.course_not_of_student = Course.objects.create( + name="Test Course 2", + ) + + self.student.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.user) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with a course not of the student + + def test_create_project_course_not_of_student(self): + response = self.client.post( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course_not_of_student.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + + def test_retrieve_project_course_not_of_student(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_course_not_of_student(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + + def test_update_project_course_not_of_student(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course_not_of_student.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) \ No newline at end of file From c3e2adc5a7f1dcd59d17d7c6992f2fb7547c73d3 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Mon, 11 Mar 2024 10:38:31 +0100 Subject: [PATCH 33/58] fix views invalid objects --- backend/pigeonhole/apps/projects/views.py | 35 ++++++++- .../tests/test_views/test_project.py | 73 ++++++++++++------- 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index ba735ab4..7b37a4c9 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -18,14 +18,21 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): course_id = kwargs.get('course_id') serializer = ProjectSerializer(Project.objects.filter(course_id=course_id), many=True) + # Check whether the course exists if not Course.objects.filter(course_id=course_id).exists(): return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, *args, **kwargs): print("ceating new project") course_id = kwargs.get('course_id') + + # Check whether the course exists + if not Course.objects.filter(course_id=course_id).exists(): + return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + serializer = ProjectSerializer(data=request.data) if serializer.is_valid(): serializer.save() @@ -37,6 +44,14 @@ def destroy(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') + # Check whether the course exists + if not Course.objects.filter(course_id=course_id).exists(): + return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + + # Check whether the project exists + if not Project.objects.filter(pk=project_id).exists(): + return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) + project = Project.objects.get(pk=project_id) project.delete() return Response({"message": "Project has been deleted successfully."}, status=status.HTTP_204_NO_CONTENT) @@ -44,11 +59,11 @@ def destroy(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') - + # Check whether the course exists if not Course.objects.filter(course_id=course_id).exists(): return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) - + # Check whether the project exists if not Project.objects.filter(pk=project_id).exists(): return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) @@ -61,6 +76,14 @@ def update(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') + # Check whether the course exists + if not Course.objects.filter(course_id=course_id).exists(): + return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + + # Check whether the project exists + if not Project.objects.filter(pk=project_id).exists(): + return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) + project = Project.objects.get(pk=project_id) serializer = ProjectSerializer(project, data=request.data) if serializer.is_valid(): @@ -69,6 +92,14 @@ def update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs): instance = self.get_object() + # Check whether the course exists + if not Course.objects.filter(course_id=instance.course_id.course_id).exists(): + return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + + # Check whether the project exists + if not Project.objects.filter(pk=instance.project_id).exists(): + return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) + serializer = self.get_serializer(instance, data=request.data, partial=True) if serializer.is_valid(): serializer.save() diff --git a/backend/pigeonhole/tests/test_views/test_project.py b/backend/pigeonhole/tests/test_views/test_project.py index 2fad0a71..f74d92c1 100644 --- a/backend/pigeonhole/tests/test_views/test_project.py +++ b/backend/pigeonhole/tests/test_views/test_project.py @@ -107,7 +107,7 @@ def test_create_project_invalid_course(self): }, format='json' ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Project.objects.count(), 1) def test_retrieve_project_invalid_course(self): @@ -121,16 +121,42 @@ def test_list_projects_invalid_course(self): API_ENDPOINT + f'100/projects/' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - + # test with invalid project - + def test_retrieve_invalid_project(self): response = self.client.get( API_ENDPOINT + f'{self.course.course_id}/projects/100/' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_partial_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_delete_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) class ProjectTestTeacher(TestCase): def setUp(self): @@ -280,15 +306,16 @@ def test_list_projects_course_not_of_teacher(self): API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + # test with invalid project - + def test_retrieve_invalid_project(self): response = self.client.get( API_ENDPOINT + f'{self.course.course_id}/projects/100/' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + class ProjectTestStudent(TestCase): def setUp(self): self.client = APIClient() @@ -334,7 +361,6 @@ def test_create_project(self): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.count(), 1) - def test_retrieve_project(self): response = self.client.get( @@ -362,14 +388,14 @@ def test_update_project(self): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - + def test_delete_project(self): response = self.client.delete( API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.count(), 1) - + def test_partial_update_project(self): response = self.client.patch( API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', @@ -380,10 +406,9 @@ def test_partial_update_project(self): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - - + # tests with an invalid course - + def test_create_project_invalid_course(self): response = self.client.post( API_ENDPOINT + f'100/projects/', @@ -396,21 +421,21 @@ def test_create_project_invalid_course(self): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.count(), 1) - + def test_retrieve_project_invalid_course(self): response = self.client.get( API_ENDPOINT + f'100/projects/{self.project.project_id}/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + def test_list_projects_invalid_course(self): response = self.client.get( API_ENDPOINT + f'100/projects/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + # tests with a course not of the student - + def test_create_project_course_not_of_student(self): response = self.client.post( API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/', @@ -423,21 +448,19 @@ def test_create_project_course_not_of_student(self): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.count(), 1) - - + def test_retrieve_project_course_not_of_student(self): response = self.client.get( API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + def test_list_projects_course_not_of_student(self): response = self.client.get( API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - + def test_update_project_course_not_of_student(self): response = self.client.patch( API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/', @@ -450,15 +473,15 @@ def test_update_project_course_not_of_student(self): ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - + # test with invalid project - + def test_retrieve_invalid_project(self): response = self.client.get( API_ENDPOINT + f'{self.course.course_id}/projects/100/' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - + def test_update_invalid_project(self): response = self.client.patch( API_ENDPOINT + f'{self.course.course_id}/projects/100/', @@ -470,9 +493,9 @@ def test_update_invalid_project(self): format='json' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + def test_delete_invalid_project(self): response = self.client.delete( API_ENDPOINT + f'{self.course.course_id}/projects/100/' ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) \ No newline at end of file + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) From 629ef6665c77afb0e70ef463e2bcb4cfe3b0d975 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Mon, 11 Mar 2024 10:51:12 +0100 Subject: [PATCH 34/58] cleanup projects view --- backend/pigeonhole/apps/projects/views.py | 46 ++++++------------- .../tests/test_views/test_project.py | 36 ++++++++++++++- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index 7b37a4c9..eb8f2c57 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -2,6 +2,7 @@ from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from django.shortcuts import get_object_or_404 from .models import Project, ProjectSerializer, Course from .permissions import CanAccessProject @@ -20,18 +21,16 @@ def list(self, request, *args, **kwargs): serializer = ProjectSerializer(Project.objects.filter(course_id=course_id), many=True) # Check whether the course exists - if not Course.objects.filter(course_id=course_id).exists(): - return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + get_object_or_404(Course, course_id=course_id) return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, *args, **kwargs): - print("ceating new project") + print("creating new project") course_id = kwargs.get('course_id') # Check whether the course exists - if not Course.objects.filter(course_id=course_id).exists(): - return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + get_object_or_404(Course, course_id=course_id) serializer = ProjectSerializer(data=request.data) if serializer.is_valid(): @@ -45,14 +44,10 @@ def destroy(self, request, *args, **kwargs): project_id = kwargs.get('pk') # Check whether the course exists - if not Course.objects.filter(course_id=course_id).exists(): - return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + get_object_or_404(Course, course_id=course_id) # Check whether the project exists - if not Project.objects.filter(pk=project_id).exists(): - return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) - - project = Project.objects.get(pk=project_id) + project = get_object_or_404(Project, pk=project_id) project.delete() return Response({"message": "Project has been deleted successfully."}, status=status.HTTP_204_NO_CONTENT) @@ -61,14 +56,11 @@ def retrieve(self, request, *args, **kwargs): project_id = kwargs.get('pk') # Check whether the course exists - if not Course.objects.filter(course_id=course_id).exists(): - return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) + get_object_or_404(Course, course_id=course_id) # Check whether the project exists - if not Project.objects.filter(pk=project_id).exists(): - return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) - - serializer = ProjectSerializer(instance=Project.objects.get(pk=project_id), many=False) + project = get_object_or_404(Project, pk=project_id) + serializer = ProjectSerializer(instance=project, many=False) return Response(serializer.data, status=status.HTTP_200_OK) @@ -76,15 +68,7 @@ def update(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') - # Check whether the course exists - if not Course.objects.filter(course_id=course_id).exists(): - return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) - - # Check whether the project exists - if not Project.objects.filter(pk=project_id).exists(): - return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) - - project = Project.objects.get(pk=project_id) + project = get_object_or_404(Project, pk=project_id) serializer = ProjectSerializer(project, data=request.data) if serializer.is_valid(): serializer.save() @@ -93,13 +77,11 @@ def update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs): instance = self.get_object() # Check whether the course exists - if not Course.objects.filter(course_id=instance.course_id.course_id).exists(): - return Response({"message": "Course does not exist."}, status=status.HTTP_404_NOT_FOUND) - + get_object_or_404(Course, course_id=instance.course_id.course_id) + # Check whether the project exists - if not Project.objects.filter(pk=instance.project_id).exists(): - return Response({"message": "Project does not exist."}, status=status.HTTP_404_NOT_FOUND) - + get_object_or_404(Project, pk=instance.project_id) + serializer = self.get_serializer(instance, data=request.data, partial=True) if serializer.is_valid(): serializer.save() diff --git a/backend/pigeonhole/tests/test_views/test_project.py b/backend/pigeonhole/tests/test_views/test_project.py index f74d92c1..c42e2fb2 100644 --- a/backend/pigeonhole/tests/test_views/test_project.py +++ b/backend/pigeonhole/tests/test_views/test_project.py @@ -97,6 +97,7 @@ def test_partial_update_project(self): self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") # tests with an invalid course + def test_create_project_invalid_course(self): response = self.client.post( API_ENDPOINT + f'100/projects/', @@ -109,7 +110,40 @@ def test_create_project_invalid_course(self): ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Project.objects.count(), 1) - + + """TODO + def test_update_project_invalid_course(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + """ + def test_delete_project_invalid_course(self): + response = self.client.delete( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.count(), 1) + + """TODO + def test_partial_update_project_invalid_course(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + """ def test_retrieve_project_invalid_course(self): response = self.client.get( API_ENDPOINT + f'100/projects/{self.project.project_id}/' From 853bdacb4925ce63229d7820731124b937a7df91 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Mon, 11 Mar 2024 11:18:15 +0100 Subject: [PATCH 35/58] Update test_project.py --- .../tests/test_views/test_project.py | 182 +++++++++++++++++- 1 file changed, 178 insertions(+), 4 deletions(-) diff --git a/backend/pigeonhole/tests/test_views/test_project.py b/backend/pigeonhole/tests/test_views/test_project.py index c42e2fb2..1cd2ddb4 100644 --- a/backend/pigeonhole/tests/test_views/test_project.py +++ b/backend/pigeonhole/tests/test_views/test_project.py @@ -97,7 +97,7 @@ def test_partial_update_project(self): self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") # tests with an invalid course - + def test_create_project_invalid_course(self): response = self.client.post( API_ENDPOINT + f'100/projects/', @@ -110,7 +110,7 @@ def test_create_project_invalid_course(self): ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Project.objects.count(), 1) - + """TODO def test_update_project_invalid_course(self): response = self.client.patch( @@ -125,13 +125,14 @@ def test_update_project_invalid_course(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") """ + def test_delete_project_invalid_course(self): response = self.client.delete( API_ENDPOINT + f'100/projects/{self.project.project_id}/' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Project.objects.count(), 1) - + """TODO def test_partial_update_project_invalid_course(self): response = self.client.patch( @@ -144,6 +145,7 @@ def test_partial_update_project_invalid_course(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") """ + def test_retrieve_project_invalid_course(self): response = self.client.get( API_ENDPOINT + f'100/projects/{self.project.project_id}/' @@ -175,7 +177,7 @@ def test_update_invalid_project(self): format='json' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - + def test_partial_update_invalid_project(self): response = self.client.patch( API_ENDPOINT + f'{self.course.course_id}/projects/100/', @@ -192,6 +194,7 @@ def test_delete_invalid_project(self): ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + class ProjectTestTeacher(TestCase): def setUp(self): self.client = APIClient() @@ -533,3 +536,174 @@ def test_delete_invalid_project(self): API_ENDPOINT + f'{self.course.course_id}/projects/100/' ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class ProjectTestUnauthenticated(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + username="student_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + ) + + self.student = Student.objects.create( + id=self.user, + number=1234567890 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.student.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + def test_create_project_unauthenticated(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_project_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with an invalid course + def test_create_project_invalid_course_unauthenticated(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_invalid_course_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_project_invalid_course_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_project_invalid_course_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # test with invalid project + + def test_retrieve_invalid_project_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_invalid_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_invalid_project_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_invalid_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) From 2d00aab340e32f0ac675de42d650abd5d8b7584b Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Mon, 11 Mar 2024 11:21:14 +0100 Subject: [PATCH 36/58] fix lint --- .../apps/projects/migrations/0001_initial.py | 4 +-- .../submissions/migrations/0001_initial.py | 8 +++-- ...ions_file_alter_submissions_output_test.py | 2 +- .../apps/users/migrations/0001_initial.py | 36 ++++++++++++++----- backend/pigeonhole/apps/users/models.py | 1 + backend/pigeonhole/settings.py | 3 +- backend/pigeonhole/urls.py | 10 +++--- 7 files changed, 41 insertions(+), 23 deletions(-) diff --git a/backend/pigeonhole/apps/projects/migrations/0001_initial.py b/backend/pigeonhole/apps/projects/migrations/0001_initial.py index 46f86d20..d849fb34 100644 --- a/backend/pigeonhole/apps/projects/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/projects/migrations/0001_initial.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -38,7 +37,8 @@ class Migration(migrations.Migration): ('condition', models.TextField(max_length=256)), ('test_file_location', models.CharField(max_length=512, null=True)), ('test_file_type', models.CharField(max_length=256, null=True)), - ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), + ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + to='projects.project')), ], ), migrations.CreateModel( diff --git a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py index 8c4d718c..715328f3 100644 --- a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -18,9 +17,12 @@ class Migration(migrations.Migration): fields=[ ('submission_id', models.BigAutoField(primary_key=True, serialize=False)), ('submission_nr', models.IntegerField()), - ('file', models.FileField(null=True, upload_to='uploads///')), + ('file', models.FileField(null=True, upload_to='uploads//' + '/')), ('timestamp', models.DateTimeField(auto_now_add=True)), - ('output_test', models.FileField(null=True, upload_to='uploads///output_test/')), + ('output_test', models.FileField(null=True, upload_to='uploads///output_test/')), ('group_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='groups.group')), ], ), diff --git a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py index 483e5352..56752ac0 100644 --- a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py +++ b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py @@ -23,4 +23,4 @@ class Migration(migrations.Migration): upload_to='uploads///output_test/'), ), - ] \ No newline at end of file + ] diff --git a/backend/pigeonhole/apps/users/migrations/0001_initial.py b/backend/pigeonhole/apps/users/migrations/0001_initial.py index 016b1bb0..36858182 100644 --- a/backend/pigeonhole/apps/users/migrations/0001_initial.py +++ b/backend/pigeonhole/apps/users/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -24,16 +23,33 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has ' + 'all permissions without explicitly ' + 'assigning them.', + verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, + help_text='Required. 150 characters or fewer. Letters, digits and ' + '@/./+/-/_ only.', max_length=150, unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name='username')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into ' + 'this admin site.', + verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be ' + 'treated as active. Unselect this instead ' + 'of deleting accounts.', + verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will ' + 'get all permissions granted to each of their ' + 'groups.', related_name='user_set', + related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', + related_name='user_set', related_query_name='user', + to='auth.permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'user', @@ -48,7 +64,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Student', fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, + to=settings.AUTH_USER_MODEL)), ('number', models.IntegerField()), ('course', models.ManyToManyField(to='courses.course')), ], @@ -56,7 +73,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Teacher', fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, + to=settings.AUTH_USER_MODEL)), ('is_admin', models.BooleanField(default=False)), ('is_assistent', models.BooleanField(default=False)), ('course', models.ManyToManyField(to='courses.course')), diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index 7d8dcd6e..be3154de 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -30,6 +30,7 @@ class Student(models.Model): class StudentSerializer(serializers.ModelSerializer): id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + class Meta: model = Student fields = ['number', 'course', 'id'] diff --git a/backend/pigeonhole/settings.py b/backend/pigeonhole/settings.py index 8c5525e1..fa9fe0e9 100644 --- a/backend/pigeonhole/settings.py +++ b/backend/pigeonhole/settings.py @@ -162,11 +162,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - SWAGGER_SETTINGS = { 'SECURITY_DEFINITIONS': { 'Basic': { 'type': 'basic' } } -} \ No newline at end of file +} diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 50d52d06..5a76951a 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,16 +1,14 @@ -from django.urls import include, path from django.contrib import admin +from django.urls import include, path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view from rest_framework import routers, permissions -from backend.testapi import views as test_views from backend.pigeonhole.apps.courses.views import CourseViewSet +from backend.pigeonhole.apps.groups.views import GroupViewSet from backend.pigeonhole.apps.projects.views import ProjectViewSet from backend.pigeonhole.apps.submissions.views import SubmissionsViewset -from backend.pigeonhole.apps.groups.views import GroupViewSet from backend.pigeonhole.apps.users.views import StudentViewSet, UserViewSet -#from backend.pigeonhole.apps.projects import views as project_views -from drf_yasg.views import get_schema_view -from drf_yasg import openapi schema_view = get_schema_view( openapi.Info( From 2c8ee17b7716f0534c2c5321112a5242c3748efe Mon Sep 17 00:00:00 2001 From: gilles-arnout Date: Mon, 11 Mar 2024 11:36:57 +0100 Subject: [PATCH 37/58] splitting client and server components --- frontend/src/app/components/CASButton.tsx | 27 +++++ frontend/src/app/components/LoginCard.tsx | 51 +++++++++ frontend/src/app/components/LoginForm.tsx | 64 +++++++++++ frontend/src/app/page.tsx | 4 +- frontend/src/components/ui/LoginForm.tsx | 124 ---------------------- frontend/src/styles/theme.ts | 44 ++++++++ 6 files changed, 188 insertions(+), 126 deletions(-) create mode 100644 frontend/src/app/components/CASButton.tsx create mode 100644 frontend/src/app/components/LoginCard.tsx create mode 100644 frontend/src/app/components/LoginForm.tsx delete mode 100644 frontend/src/components/ui/LoginForm.tsx create mode 100644 frontend/src/styles/theme.ts diff --git a/frontend/src/app/components/CASButton.tsx b/frontend/src/app/components/CASButton.tsx new file mode 100644 index 00000000..6af7e5c7 --- /dev/null +++ b/frontend/src/app/components/CASButton.tsx @@ -0,0 +1,27 @@ +"use client"; +import React from 'react' +import SchoolIcon from "@mui/icons-material/School"; +import {Button} from "@mui/material"; + +const CASButton = () => { + const handleCASLogin = (): void => { + // Implement CAS login logic here + console.log('Login with CAS'); + }; + + return ( +
+ +
+ ) +} + +export default CASButton diff --git a/frontend/src/app/components/LoginCard.tsx b/frontend/src/app/components/LoginCard.tsx new file mode 100644 index 00000000..af1b2b86 --- /dev/null +++ b/frontend/src/app/components/LoginCard.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import {Box, Container, CssBaseline, Typography} from '@mui/material'; +import {ThemeProvider} from '@mui/material/styles'; +import CASButton from "@/app/components/CASButton"; +import LoginForm from "@/app/components/LoginForm"; +import loginTheme from '../../styles/theme'; + +const LoginCard: React.FC = () => { + return ( + + + + + + Pigeonhole + +
+ + + + OR + + + +
+
+
+
+ ); +}; + +export default LoginCard; diff --git a/frontend/src/app/components/LoginForm.tsx b/frontend/src/app/components/LoginForm.tsx new file mode 100644 index 00000000..abb5f37c --- /dev/null +++ b/frontend/src/app/components/LoginForm.tsx @@ -0,0 +1,64 @@ +"use client"; +import React, {useState} from 'react'; +import {Button, IconButton, InputAdornment, TextField} from "@mui/material"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; + +const LoginForm = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + + const handleLogin = (): void => { + // Implement your login logic here + console.log('Login with:', email, password); + }; + + const handleClickShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleMouseDownPassword = (event: React.MouseEvent) => { + event.preventDefault(); + }; + + return ( +
+ setEmail(e.target.value)} + fullWidth + /> + setPassword(e.target.value)} + fullWidth + InputProps={{ + endAdornment: ( + + + {showPassword ? ( + // Set the icon to small + ) : ( + // Set the icon to small + )} + + + ), + }} + /> + +
+ ); +} + +export default LoginForm; diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index b7f71cda..66d47abf 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import LoginForm from '@/components/ui/LoginForm'; +import LoginCard from '@/app/components/LoginCard'; const Login = () => { return (
- +
) } diff --git a/frontend/src/components/ui/LoginForm.tsx b/frontend/src/components/ui/LoginForm.tsx deleted file mode 100644 index 49008083..00000000 --- a/frontend/src/components/ui/LoginForm.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client" -import React, {useState} from 'react'; -import {Box, Button, Container, CssBaseline, TextField, Typography} from '@mui/material'; -import {createTheme, ThemeProvider} from '@mui/material/styles'; -import SchoolIcon from '@mui/icons-material/School'; - -const theme = createTheme({ - palette: { - background: { - default: '#f4f5fd' - }, - primary: { - main: '#1976d2', - }, - secondary: { - main: '#9c27b0', - }, - }, - typography: { - fontFamily: 'Quicksand, sans-serif', - h4: { - fontWeight: 700, - }, - }, - components: { - MuiTextField: { - defaultProps: { - InputLabelProps: { - shrink: true, - }, - margin: 'normal', - required: true, - fullWidth: true, - }, - }, - MuiButton: { - defaultProps: { - variant: 'contained', - color: 'primary', - fullWidth: true, - style: {margin: '10px 0'}, - }, - }, - }, -}); - -const LoginForm: React.FC = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - - const handleLogin = (): void => { - // Implement your login logic here - console.log('Login with:', email, password); - }; - - const handleCASLogin = (): void => { - // Implement CAS login logic here - console.log('Login with CAS'); - }; - - return ( - - - - - - Pigeonhole - -
- setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> - - - - OR - - - - -
-
-
- ); -}; - -export default LoginForm; diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts new file mode 100644 index 00000000..3ff4e7f9 --- /dev/null +++ b/frontend/src/styles/theme.ts @@ -0,0 +1,44 @@ +"use client"; +import { createTheme } from '@mui/material/styles'; + +const loginTheme = createTheme({ + palette: { + background: { + default: '#f4f5fd' + }, + primary: { + main: '#1976d2', + }, + secondary: { + main: '#9c27b0', + }, + }, + typography: { + fontFamily: 'Quicksand, sans-serif', + h4: { + fontWeight: 700, + }, + }, + components: { + MuiTextField: { + defaultProps: { + InputLabelProps: { + shrink: true, + }, + margin: 'normal', + required: true, + fullWidth: true, + }, + }, + MuiButton: { + defaultProps: { + variant: 'contained', + color: 'primary', + fullWidth: true, + style: {margin: '10px 0'}, + }, + }, + }, +}); + +export default loginTheme; From 6da1e6d97a5fe89a2259724f14ce055cc1ac3236 Mon Sep 17 00:00:00 2001 From: robinpdev Date: Mon, 11 Mar 2024 13:13:19 +0100 Subject: [PATCH 38/58] user uploads toevoegen --- .gitignore | 1 + Makefile | 5 ++++- backend/pigeonhole/urls.py | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3ed07993..e4642805 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env django/db.sqlite3 backend/db.sqlite3 +backend/uploads venv/ .venv/ diff --git a/Makefile b/Makefile index 01c1abef..9c0b630b 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,7 @@ stop: lint: docker exec pigeonhole-backend flake8 . - docker exec pigeonhole-frontend npm run lint \ No newline at end of file + docker exec pigeonhole-frontend npm run lint + +backendshell: + docker exec -it pigeonhole-backend sh \ No newline at end of file diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 5a76951a..d9d206f9 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,5 +1,7 @@ from django.contrib import admin from django.urls import include, path +from django.conf import settings +from django.conf.urls.static import static from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework import routers, permissions @@ -39,6 +41,6 @@ path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), path("admin/", admin.site.urls), -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += router.urls From 49824957a56938954928ba803ac0580d35574208 Mon Sep 17 00:00:00 2001 From: robinpdev Date: Mon, 11 Mar 2024 13:32:40 +0100 Subject: [PATCH 39/58] add testing workflow --- .github/workflows/test.yml | 23 +++++++++++++++++++++++ Makefile | 5 ++++- backend/runtests.sh | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 backend/runtests.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..0def4b64 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Backend test CI + +on: + - pull_request + +jobs: + flake8: + runs-on: self-hosted + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + working-directory: ./backend + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Running Django tests + working-directory: ./ + run: | + sh run backend/runtests.sh \ No newline at end of file diff --git a/Makefile b/Makefile index 01c1abef..2d57da69 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,7 @@ stop: lint: docker exec pigeonhole-backend flake8 . - docker exec pigeonhole-frontend npm run lint \ No newline at end of file + docker exec pigeonhole-frontend npm run lint + +backendtest: + docker exec -it pigeonhole-backend sh /usr/src/app/backend/runtests.sh \ No newline at end of file diff --git a/backend/runtests.sh b/backend/runtests.sh new file mode 100644 index 00000000..65d41fa8 --- /dev/null +++ b/backend/runtests.sh @@ -0,0 +1 @@ +coverage run manage.py test backend/pigeonhole/tests/test_views \ No newline at end of file From 6c6cc3908c99dde2151dc5e11c94dc3ea4ced461 Mon Sep 17 00:00:00 2001 From: robinpdev Date: Mon, 11 Mar 2024 13:34:59 +0100 Subject: [PATCH 40/58] try fixing test workflow --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0def4b64..f0fd0d19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,5 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - name: Running Django tests - working-directory: ./ run: | - sh run backend/runtests.sh \ No newline at end of file + sh run ./backend/runtests.sh \ No newline at end of file From 057c25ed5132bd80cdaf2044cb44d7a79a961238 Mon Sep 17 00:00:00 2001 From: robinpdev Date: Mon, 11 Mar 2024 13:42:33 +0100 Subject: [PATCH 41/58] same --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0fd0d19..ef85c7b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: - pull_request jobs: - flake8: + backend-test: runs-on: self-hosted steps: - uses: actions/checkout@v3 @@ -13,10 +13,10 @@ jobs: with: python-version: "3.11" - name: Install dependencies - working-directory: ./backend + working-directory: ./ run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r ./backend/requirements.txt - name: Running Django tests run: | sh run ./backend/runtests.sh \ No newline at end of file From c297348eb5d81162c9734695f54c0c1f21f63d2b Mon Sep 17 00:00:00 2001 From: robinpdev Date: Mon, 11 Mar 2024 13:45:20 +0100 Subject: [PATCH 42/58] lol --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef85c7b8..10d45d48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,4 +19,4 @@ jobs: pip install -r ./backend/requirements.txt - name: Running Django tests run: | - sh run ./backend/runtests.sh \ No newline at end of file + sh ./backend/runtests.sh \ No newline at end of file From a0f5a924c6fa18354b682117dffa001a73419808 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Mon, 11 Mar 2024 14:20:56 +0100 Subject: [PATCH 43/58] users changed --- .../pigeonhole/apps/courses/permissions.py | 30 ++++----- .../0005_remove_group_student_group_user.py | 24 +++++++ backend/pigeonhole/apps/groups/models.py | 6 +- .../pigeonhole/apps/projects/permissions.py | 24 +++---- ...acher_course_remove_teacher_id_and_more.py | 64 +++++++++++++++++++ backend/pigeonhole/apps/users/models.py | 58 ++++++++--------- backend/pigeonhole/apps/users/views.py | 24 +------ .../tests/test_models/test_conditions.py | 6 +- .../tests/test_models/test_course.py | 10 +-- .../tests/test_models/test_groups.py | 12 ++-- .../tests/test_models/test_project.py | 10 +-- .../tests/test_models/test_submissions.py | 6 +- .../pigeonhole/tests/test_models/test_user.py | 28 ++++---- backend/pigeonhole/urls.py | 3 +- 14 files changed, 183 insertions(+), 122 deletions(-) create mode 100644 backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py create mode 100644 backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py index 201b666e..13119e03 100644 --- a/backend/pigeonhole/apps/courses/permissions.py +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -1,30 +1,30 @@ from rest_framework import permissions -from backend.pigeonhole.apps.users.models import Student, Teacher +from backend.pigeonhole.apps.users.models import User class CourseUserPermissions(permissions.BasePermission): def has_permission(self, request, view): if request.user.is_superuser: return True - if isinstance(request.user, Teacher): - return True - - if isinstance(request.user, Student): - return view.action in ['list', 'retrieve'] + # if isinstance(request.user, Teacher): + # return True + # + # if isinstance(request.user, Student): + # return view.action in ['list', 'retrieve'] return False def has_object_permission(self, request, view, obj): if request.user.is_superuser: return True - if isinstance(request.user, Teacher): - if request.user.is_admin: - return True - elif Teacher.objects.filter(id=request.user.id, course=obj).exists(): - return True - return view.action in ['list', 'retrieve'] - - if isinstance(request.user, Student): - return view.action in ['list', 'retrieve'] + # if isinstance(request.user, Teacher): + # if request.user.is_admin: + # return True + # elif Teacher.objects.filter(id=request.user.id, course=obj).exists(): + # return True + # return view.action in ['list', 'retrieve'] + # + # if isinstance(request.user, Student): + # return view.action in ['list', 'retrieve'] return False diff --git a/backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py b/backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py new file mode 100644 index 00000000..34f5581e --- /dev/null +++ b/backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.2 on 2024-03-11 13:17 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('groups', '0004_alter_group_final_score'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='group', + name='student', + ), + migrations.AddField( + model_name='group', + name='user', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/pigeonhole/apps/groups/models.py b/backend/pigeonhole/apps/groups/models.py index c7df2aee..164da31b 100644 --- a/backend/pigeonhole/apps/groups/models.py +++ b/backend/pigeonhole/apps/groups/models.py @@ -2,14 +2,14 @@ from rest_framework import serializers from backend.pigeonhole.apps.projects.models import Project -from backend.pigeonhole.apps.users.models import Student +from backend.pigeonhole.apps.users.models import User class Group(models.Model): group_id = models.BigAutoField(primary_key=True) group_nr = models.IntegerField(blank=True, null=True) project_id = models.ForeignKey(Project, on_delete=models.CASCADE) - student = models.ManyToManyField(Student) + user = models.ManyToManyField(User) feedback = models.TextField(null=True) final_score = models.IntegerField(null=True, blank=True) @@ -28,4 +28,4 @@ def save(self, *args, **kwargs): class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group - fields = ["group_id", "group_nr", "final_score", "project_id", "student", "feedback"] + fields = ["group_id", "group_nr", "final_score", "project_id", "user", "feedback"] diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index d08a56b5..77efdd16 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -1,5 +1,5 @@ from rest_framework import permissions -from backend.pigeonhole.apps.users.models import Teacher, Student +from backend.pigeonhole.apps.users.models import User class CanAccessProject(permissions.BasePermission): @@ -9,15 +9,15 @@ def has_permission(self, request, view): user = request.user subject_id = view.kwargs.get('course_id') # If the user is a teacher, grant access. - if isinstance(user, Teacher): - if user.course.filter(id=subject_id).exists(): - return True - elif isinstance(user, Teacher) and user.is_admin: - return True - # If the user is a student, grant access only to their own projects. - elif isinstance(user, Student): - if user.course.filter(id=subject_id).exists(): - return True - elif request.user.is_superuser: - return True + # if isinstance(user, Teacher): + # if user.course.filter(id=subject_id).exists(): + # return True + # elif isinstance(user, Teacher) and user.is_admin: + # return True + # # If the user is a student, grant access only to their own projects. + # elif isinstance(user, Student): + # if user.course.filter(id=subject_id).exists(): + # return True + # elif request.user.is_superuser: + # return True return False diff --git a/backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py b/backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py new file mode 100644 index 00000000..67dd6291 --- /dev/null +++ b/backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 5.0.2 on 2024-03-11 13:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0001_initial'), + ('groups', '0005_remove_group_student_group_user'), + ('users', '0002_rename_is_assistent_teacher_is_assistant'), + ] + + operations = [ + migrations.RemoveField( + model_name='teacher', + name='course', + ), + migrations.RemoveField( + model_name='teacher', + name='id', + ), + migrations.AlterModelManagers( + name='user', + managers=[ + ], + ), + migrations.AddField( + model_name='user', + name='course', + field=models.ManyToManyField(to='courses.course'), + ), + migrations.AddField( + model_name='user', + name='role', + field=models.IntegerField(choices=[(1, 'Superuser'), (2, 'Teacher'), (3, 'Student')], default=3), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True), + ), + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(max_length=30), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='user', + name='last_name', + field=models.CharField(max_length=150), + ), + migrations.DeleteModel( + name='Student', + ), + migrations.DeleteModel( + name='Teacher', + ), + ] diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index be3154de..80247fda 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -5,47 +5,43 @@ from backend.pigeonhole.apps.courses.models import Course -class User(AbstractUser): - class Meta(AbstractUser.Meta): - db_table = "auth_user" - - @property - def name(self): - return f"{self.first_name.strip()} {self.last_name.strip()}" +class Roles(models.IntegerChoices): + SUPERUSER = 1 + TEACHER = 2 + STUDENT = 3 -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ['id', 'email', 'first_name', 'last_name'] - - -class Student(models.Model): - id = models.ForeignKey(User, on_delete=models.CASCADE, primary_key=True) - number = models.IntegerField() +class User(AbstractUser): + id = models.BigAutoField(primary_key=True) + email = models.EmailField(unique=True) + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=150) course = models.ManyToManyField(Course) + role = models.IntegerField(choices=Roles.choices, default=Roles.STUDENT) objects = models.Manager() + class Meta(AbstractUser.Meta): + db_table = "auth_user" -class StudentSerializer(serializers.ModelSerializer): - id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) - - class Meta: - model = Student - fields = ['number', 'course', 'id'] + @property + def name(self): + return f"{self.first_name.strip()} {self.last_name.strip()}" + @property + def is_super(self): + return self.role == Roles.SUPERUSER -class Teacher(models.Model): - id = models.ForeignKey(User, on_delete=models.CASCADE, primary_key=True) - course = models.ManyToManyField(Course) - is_admin = models.BooleanField(default=False) - is_assistant = models.BooleanField(default=False) + @property + def is_teacher(self): + return self.role == Roles.TEACHER - objects = models.Manager() + @property + def is_student(self): + return self.role == Roles.STUDENT -class TeacherSerializer(serializers.ModelSerializer): +class UserSerializer(serializers.ModelSerializer): class Meta: - model = Teacher - fields = ['course', 'id', 'is_admin', 'is_assistent'] + model = User + fields = ['id', 'email', 'first_name', 'last_name', 'course', 'role'] diff --git a/backend/pigeonhole/apps/users/views.py b/backend/pigeonhole/apps/users/views.py index 0af8843f..5fec4879 100644 --- a/backend/pigeonhole/apps/users/views.py +++ b/backend/pigeonhole/apps/users/views.py @@ -2,29 +2,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from backend.pigeonhole.apps.users.models import Student, StudentSerializer, User, UserSerializer - - -class StudentViewSet(viewsets.ModelViewSet): - queryset = Student.objects.all() - serializer_class = StudentSerializer - permission_classes = [IsAuthenticated] - - def create(self, request, *args, **kwargs): - serializer = StudentSerializer(data=request.data) - print(serializer) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def perform_create(self, serializer): - serializer.save(user=self.request.user) - - def list(self, request, *args, **kwargs): - serializer = StudentSerializer(self.queryset, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) +from backend.pigeonhole.apps.users.models import User, UserSerializer class UserViewSet(viewsets.ModelViewSet): diff --git a/backend/pigeonhole/tests/test_models/test_conditions.py b/backend/pigeonhole/tests/test_models/test_conditions.py index 37307ab7..9998c3cd 100644 --- a/backend/pigeonhole/tests/test_models/test_conditions.py +++ b/backend/pigeonhole/tests/test_models/test_conditions.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.projects.models import Project, Conditions, AllowedExtension, ForbiddenExtension @@ -22,8 +22,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") diff --git a/backend/pigeonhole/tests/test_models/test_course.py b/backend/pigeonhole/tests/test_models/test_course.py index 2498a217..d576e67f 100644 --- a/backend/pigeonhole/tests/test_models/test_course.py +++ b/backend/pigeonhole/tests/test_models/test_course.py @@ -1,6 +1,6 @@ from django.test import TestCase from backend.pigeonhole.apps.courses.models import Course -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from django.db.utils import DataError @@ -25,8 +25,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -34,7 +34,7 @@ def setUp(self): student.course.add(course) def test_course_teacher_relationship(self): - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") course = Course.objects.get(name="Math") self.assertIn(course, teacher.course.all()) course_alter_ego = teacher.course.get(name="Math") @@ -42,7 +42,7 @@ def test_course_teacher_relationship(self): self.assertTrue(course_alter_ego, "Mathematics") def test_course_students_relationship(self): - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") course = Course.objects.get(name="Math") self.assertIn(course, student.course.all()) course_alter_ego = student.course.get(name="Math") diff --git a/backend/pigeonhole/tests/test_models/test_groups.py b/backend/pigeonhole/tests/test_models/test_groups.py index 0ece53b9..09f9d6bc 100644 --- a/backend/pigeonhole/tests/test_models/test_groups.py +++ b/backend/pigeonhole/tests/test_models/test_groups.py @@ -1,6 +1,6 @@ from django.test import TestCase from backend.pigeonhole.apps.courses.models import Course -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.groups.models import Group from backend.pigeonhole.apps.projects.models import Project @@ -34,9 +34,9 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) - student2 = Student.objects.create(id=student_user2, number=5678) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) + student2 = User.objects.create(id=student_user2, number=5678) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -67,8 +67,8 @@ def test_group_project_relation(self): def test_group_student_relation(self): group = Group.objects.get(group_nr=1) - student = Student.objects.get(id__email="student@gmail.com") - student2 = Student.objects.get(id__email="student2@gmail.com") + student = User.objects.get(id__email="student@gmail.com") + student2 = User.objects.get(id__email="student2@gmail.com") self.assertIn(student, group.student.all()) self.assertIn(student2, group.student.all()) diff --git a/backend/pigeonhole/tests/test_models/test_project.py b/backend/pigeonhole/tests/test_models/test_project.py index 11285c91..da680d61 100644 --- a/backend/pigeonhole/tests/test_models/test_project.py +++ b/backend/pigeonhole/tests/test_models/test_project.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.projects.models import Project @@ -22,8 +22,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") @@ -41,11 +41,11 @@ def test_project_course_relation(self): self.assertEqual(self.project.course_id.name, "Math") def test_project_teacher_relation(self): - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertIn(self.project.course_id, teacher.course.all()) def test_project_student_relation(self): - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") self.assertIn(self.project.course_id, student.course.all()) def test_course_name_length_validation(self): diff --git a/backend/pigeonhole/tests/test_models/test_submissions.py b/backend/pigeonhole/tests/test_models/test_submissions.py index c6efed08..31b946a4 100644 --- a/backend/pigeonhole/tests/test_models/test_submissions.py +++ b/backend/pigeonhole/tests/test_models/test_submissions.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.projects.models import Project from backend.pigeonhole.apps.submissions.models import Submissions @@ -25,8 +25,8 @@ def setUp(self): ) # Create teacher and student using the created users - teacher = Teacher.objects.create(id=teacher_user) - student = Student.objects.create(id=student_user, number=1234) + teacher = User.objects.create(id=teacher_user) + student = User.objects.create(id=student_user, number=1234) # Create course course = Course.objects.create(name="Math", description="Mathematics") diff --git a/backend/pigeonhole/tests/test_models/test_user.py b/backend/pigeonhole/tests/test_models/test_user.py index 043197e5..383933fd 100644 --- a/backend/pigeonhole/tests/test_models/test_user.py +++ b/backend/pigeonhole/tests/test_models/test_user.py @@ -1,5 +1,5 @@ from django.test import TestCase -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User # python3 manage.py test backend/ @@ -22,51 +22,51 @@ def setUp(self): ) # Create teacher and student using the created users - Teacher.objects.create(id=teacher_user) - Student.objects.create(id=student_user, number=1234) + User.objects.create(id=teacher_user) + User.objects.create(id=student_user, number=1234) def test_student(self): - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") self.assertEqual(student.id.email, "student@gmail.com") self.assertEqual(student.number, 1234) # update student number student.number = 5678 student.save() - student = Student.objects.get(id__email="student@gmail.com") + student = User.objects.get(id__email="student@gmail.com") self.assertEqual(student.number, 5678) # delete student student.delete() - with self.assertRaises(Student.DoesNotExist): - Student.objects.get(id__email="student@gmail.com") + with self.assertRaises(User.DoesNotExist): + User.objects.get(id__email="student@gmail.com") def test_teacher(self): - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertEqual(teacher.id.email, "teacher@gmail.com") self.assertEqual(teacher.is_admin, False) # update teacher is_admin teacher.is_admin = True teacher.save() - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertEqual(teacher.is_admin, True) self.assertEqual(teacher.is_assistant, False) # update teacher is_assistent teacher.is_assistant = True teacher.save() - teacher = Teacher.objects.get(id__email="teacher@gmail.com") + teacher = User.objects.get(id__email="teacher@gmail.com") self.assertEqual(teacher.is_assistant, True) # delete teacher teacher.delete() - with self.assertRaises(Teacher.DoesNotExist): - Teacher.objects.get(id__email="teacher@gmail.com") + with self.assertRaises(User.DoesNotExist): + User.objects.get(id__email="teacher@gmail.com") def test_create_student_without_user(self): with self.assertRaises(Exception): - Student.objects.create(number=1234) + User.objects.create(number=1234) def test_create_teacher_without_user(self): with self.assertRaises(Exception): - Teacher.objects.create(is_admin=True, is_assistent=True) + User.objects.create(is_admin=True, is_assistent=True) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 5a76951a..abf24149 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -8,7 +8,7 @@ from backend.pigeonhole.apps.groups.views import GroupViewSet from backend.pigeonhole.apps.projects.views import ProjectViewSet from backend.pigeonhole.apps.submissions.views import SubmissionsViewset -from backend.pigeonhole.apps.users.views import StudentViewSet, UserViewSet +from backend.pigeonhole.apps.users.views import UserViewSet schema_view = get_schema_view( openapi.Info( @@ -25,7 +25,6 @@ router = routers.DefaultRouter() router.register(r'users', UserViewSet) -router.register(r'students', StudentViewSet) router.register(r'groups', GroupViewSet) router.register(r'courses', CourseViewSet) router.register(r'submissions', SubmissionsViewset) From 68fc4c1e925d6a0c82de92865d0eb347eb376ce8 Mon Sep 17 00:00:00 2001 From: avoyen Date: Mon, 11 Mar 2024 18:39:44 +0100 Subject: [PATCH 44/58] naming change, permission for project api --- .../pigeonhole/apps/projects/permissions.py | 22 ++++++++----------- backend/pigeonhole/apps/users/models.py | 6 ++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index 77efdd16..587dfe0d 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -7,17 +7,13 @@ class CanAccessProject(permissions.BasePermission): # to the project data. def has_permission(self, request, view): user = request.user - subject_id = view.kwargs.get('course_id') - # If the user is a teacher, grant access. - # if isinstance(user, Teacher): - # if user.course.filter(id=subject_id).exists(): - # return True - # elif isinstance(user, Teacher) and user.is_admin: - # return True - # # If the user is a student, grant access only to their own projects. - # elif isinstance(user, Student): - # if user.course.filter(id=subject_id).exists(): - # return True - # elif request.user.is_superuser: - # return True + course_id = view.kwargs.get('course_id') + if user.is_student: + if user.course.filter(course_id=course_id).exists(): + return True + elif user.is_teacher: + if user.course.filter(course_id=course_id).exists(): + return True + elif user.is_admin or user.is_superuser: + return True return False diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index 80247fda..5f6d02c9 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -6,7 +6,7 @@ class Roles(models.IntegerChoices): - SUPERUSER = 1 + ADMIN = 1 TEACHER = 2 STUDENT = 3 @@ -29,8 +29,8 @@ def name(self): return f"{self.first_name.strip()} {self.last_name.strip()}" @property - def is_super(self): - return self.role == Roles.SUPERUSER + def is_admin(self): + return self.role == Roles.ADMIN @property def is_teacher(self): From 3aa70150a500bbad88b49d77254678f4aaf356a5 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Mon, 11 Mar 2024 19:19:50 +0100 Subject: [PATCH 45/58] permissions updated, course api expanded --- .../pigeonhole/apps/courses/permissions.py | 28 ++++++++---------- backend/pigeonhole/apps/courses/views.py | 29 ++++++++++++++++++- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py index 13119e03..47c3494a 100644 --- a/backend/pigeonhole/apps/courses/permissions.py +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -4,27 +4,23 @@ class CourseUserPermissions(permissions.BasePermission): def has_permission(self, request, view): - if request.user.is_superuser: + if request.user.is_admin or request.user.is_superuser: return True - # if isinstance(request.user, Teacher): - # return True - # - # if isinstance(request.user, Student): - # return view.action in ['list', 'retrieve'] + + if request.user.is_student or request.user.is_teacher: + return view.action in ['list', 'retrieve'] return False def has_object_permission(self, request, view, obj): - if request.user.is_superuser: + if request.user.is_admin or request.user.is_superuser: return True - # if isinstance(request.user, Teacher): - # if request.user.is_admin: - # return True - # elif Teacher.objects.filter(id=request.user.id, course=obj).exists(): - # return True - # return view.action in ['list', 'retrieve'] - # - # if isinstance(request.user, Student): - # return view.action in ['list', 'retrieve'] + if request.user.is_teacher: + if User.objects.filter(id=request.user.id, course=obj).exists(): + return True + return view.action in ['list', 'retrieve'] + + if request.user.is_student: + return view.action in ['list', 'retrieve'] return False diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py index fc1715a7..89572901 100644 --- a/backend/pigeonhole/apps/courses/views.py +++ b/backend/pigeonhole/apps/courses/views.py @@ -1,7 +1,9 @@ from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.decorators import action +from backend.pigeonhole.apps.users.models import User from .models import Course, CourseSerializer from .permissions import CourseUserPermissions @@ -34,11 +36,36 @@ def destroy(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) def list(self, request, *args, **kwargs): + if request.user.is_admin or request.user.is_superuser + serializer = CourseSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + if request.user.is_teacher or request.user.is_student: + courses = User.objects.get(id=request.user.id).course.all() + serializer = CourseSerializer(courses, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) serializer = CourseSerializer(self.queryset, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, *args, **kwargs): course_id = kwargs.get('pk') course = Course.objects.get(pk=course_id) serializer = CourseSerializer(course, many=False) return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + serializer = CourseSerializer(course, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=['post']) + def join_course(self, request, *args, **kwargs): + course_id = kwargs.get('pk') + course = Course.objects.get(pk=course_id) + user = User.objects.get(id=request.user.id) + user.course.add(course) + return Response(status=status.HTTP_200_OK) + From 8bd347b35257eb830214886bc0b6590c9347ee03 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Mon, 11 Mar 2024 19:21:26 +0100 Subject: [PATCH 46/58] course api error fixed --- backend/pigeonhole/apps/courses/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py index 89572901..506c6f9b 100644 --- a/backend/pigeonhole/apps/courses/views.py +++ b/backend/pigeonhole/apps/courses/views.py @@ -36,7 +36,7 @@ def destroy(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) def list(self, request, *args, **kwargs): - if request.user.is_admin or request.user.is_superuser + if request.user.is_admin or request.user.is_superuser: serializer = CourseSerializer(self.queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) if request.user.is_teacher or request.user.is_student: From f8e13b619640386dcc8ecfe0d50fbd02250ce07a Mon Sep 17 00:00:00 2001 From: avoyen Date: Mon, 11 Mar 2024 22:40:57 +0100 Subject: [PATCH 47/58] more checks added to projects api, added more functionality to groups endpoint + url --- backend/pigeonhole/apps/groups/permission.py | 18 +++++ backend/pigeonhole/apps/groups/views.py | 43 +++++++--- .../pigeonhole/apps/projects/permissions.py | 11 ++- backend/pigeonhole/apps/projects/views.py | 80 +++++++++++-------- backend/pigeonhole/urls.py | 2 +- 5 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 backend/pigeonhole/apps/groups/permission.py diff --git a/backend/pigeonhole/apps/groups/permission.py b/backend/pigeonhole/apps/groups/permission.py new file mode 100644 index 00000000..5672d2c3 --- /dev/null +++ b/backend/pigeonhole/apps/groups/permission.py @@ -0,0 +1,18 @@ +from rest_framework import permissions + + +class CanAccessProject(permissions.BasePermission): + # Custom user class to check if the user can join a group. + def has_permission(self, request, view): + user = request.user + course_id = view.kwargs.get('course_id') + + if user.is_admin or user.is_superuser: + return True + elif user.is_teacher: + if user.course.filter(course_id=course_id).exists(): + return True + elif user.is_student: + if user.course.filter(course_id=course_id).exists(): + return True + return False diff --git a/backend/pigeonhole/apps/groups/views.py b/backend/pigeonhole/apps/groups/views.py index 23bbeca7..5557f971 100644 --- a/backend/pigeonhole/apps/groups/views.py +++ b/backend/pigeonhole/apps/groups/views.py @@ -1,8 +1,11 @@ +from django.shortcuts import get_object_or_404 from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from backend.pigeonhole.apps.courses.models import Course from backend.pigeonhole.apps.groups.models import Group, GroupSerializer +from backend.pigeonhole.apps.projects.models import Project class GroupViewSet(viewsets.ModelViewSet): @@ -10,17 +13,39 @@ class GroupViewSet(viewsets.ModelViewSet): serializer_class = GroupSerializer permission_classes = [IsAuthenticated] - def perform_create(self, serializer): - serializer.save(user=self.request.user) + def create(self, request, *args, **kwargs): + # TODO zorg dat er geen 2 groepen met hetzelfde nummer kunnen zijn. + course_id = kwargs.get('course_id') + + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + serializer = GroupSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": "You are not allowed to create a new group."}, + status=status.HTTP_400_BAD_REQUEST) def list(self, request, *args, **kwargs): serializer = GroupSerializer(self.queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - def create(self, request, *args, **kwargs): - serializer = GroupSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def retrieve(self, request, *args, **kwargs): + course_id = kwargs.get('course_id') + project_id = kwargs.get('project_id') + group_id = kwargs.get('pk') + + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) + + # Check whether the project exists + get_object_or_404(Project, pk=project_id) + group = get_object_or_404(Group, group_id=group_id) + + serializer = GroupSerializer(instance=group, many=False) + + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index 6ff88ba8..6a1157ed 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -1,5 +1,4 @@ from rest_framework import permissions -from backend.pigeonhole.apps.users.models import User class CanAccessProject(permissions.BasePermission): @@ -8,12 +7,12 @@ class CanAccessProject(permissions.BasePermission): def has_permission(self, request, view): user = request.user course_id = view.kwargs.get('course_id') - if user.is_student: + if user.is_admin or user.is_superuser: + return True + elif user.is_teacher: if user.course.filter(course_id=course_id).exists(): return True - elif user.is_teacher: + elif user.is_student: if user.course.filter(course_id=course_id).exists(): return True - elif user.is_admin or user.is_superuser: - return True - return False \ No newline at end of file + return False diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index eb8f2c57..a929c8d2 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -1,21 +1,19 @@ +from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from django.shortcuts import get_object_or_404 from .models import Project, ProjectSerializer, Course from .permissions import CanAccessProject +# TODO hier nog zorgen als een project niet visible is, dat de students het niet kunnen zien. class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer permission_classes = [IsAuthenticated & CanAccessProject] - def perform_create(self, serializer): - serializer.save(user=self.request.user) - def list(self, request, *args, **kwargs): course_id = kwargs.get('course_id') serializer = ProjectSerializer(Project.objects.filter(course_id=course_id), many=True) @@ -26,30 +24,36 @@ def list(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request, *args, **kwargs): - print("creating new project") course_id = kwargs.get('course_id') - # Check whether the course exists - get_object_or_404(Course, course_id=course_id) + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) - serializer = ProjectSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer = ProjectSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": "You are not allowed to create a new project."}, + status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') - # Check whether the course exists - get_object_or_404(Course, course_id=course_id) + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) - # Check whether the project exists - project = get_object_or_404(Project, pk=project_id) - project.delete() - return Response({"message": "Project has been deleted successfully."}, status=status.HTTP_204_NO_CONTENT) + # Check whether the project exists + project = get_object_or_404(Project, pk=project_id) + project.delete() + return Response({"message": "Project has been deleted successfully."}, + status=status.HTTP_204_NO_CONTENT) + return Response({"message": "You are not allowed to delete this project."}, + status=status.HTTP_403_FORBIDDEN) def retrieve(self, request, *args, **kwargs): course_id = kwargs.get('course_id') @@ -68,21 +72,31 @@ def update(self, request, *args, **kwargs): course_id = kwargs.get('course_id') project_id = kwargs.get('pk') - project = get_object_or_404(Project, pk=project_id) - serializer = ProjectSerializer(project, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + get_object_or_404(Course, course_id=course_id) + + project = get_object_or_404(Project, pk=project_id) + serializer = ProjectSerializer(project, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response({"message": "You are not allowed to update this project."}, + status=status.HTTP_403_FORBIDDEN) def partial_update(self, request, *args, **kwargs): - instance = self.get_object() - # Check whether the course exists - get_object_or_404(Course, course_id=instance.course_id.course_id) + course_id = kwargs.get('course_id') + project_id = kwargs.get('pk') - # Check whether the project exists - get_object_or_404(Project, pk=instance.project_id) + if request.user.is_teacher or request.user.is_admin or request.user.is_superuser: + # Check whether the course exists + get_object_or_404(Course, course_id=course_id) - serializer = self.get_serializer(instance, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) + # Check whether the project exists + project = get_object_or_404(Project, pk=project_id) + + serializer = ProjectSerializer(project, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response({"message": "You are not allowed to update this project."}, + status=status.HTTP_403_FORBIDDEN) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 04d7004b..0c0a9680 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -27,10 +27,10 @@ router = routers.DefaultRouter() router.register(r'users', UserViewSet) -router.register(r'groups', GroupViewSet) router.register(r'courses', CourseViewSet) router.register(r'submissions', SubmissionsViewset) router.register(r'courses/(?P[^/.]+)/projects', ProjectViewSet) +router.register(r'courses/(?P[^/.]+)/projects/(?P[^/.]+)/groups', GroupViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. From f70db26a6cb1fb7ade234f52f1cecc54f7c37c9f Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Tue, 12 Mar 2024 09:03:46 +0100 Subject: [PATCH 48/58] refactor testen --- .../pigeonhole/apps/courses/permissions.py | 3 + .../pigeonhole/apps/projects/permissions.py | 2 +- .../tests/test_views/test_course.py | 245 ------ .../tests/test_views/test_course/__init__.py | 0 .../tests/test_views/test_course/course.txt | 91 +++ .../test_views/test_course/test_admin.py | 77 ++ .../test_views/test_course/test_teacher.py | 82 ++ .../tests/test_views/test_project.py | 709 ------------------ .../tests/test_views/test_project/__init__.py | 0 .../test_views/test_project/test_admin.py | 189 +++++ .../test_views/test_project/test_student.py | 190 +++++ .../test_views/test_project/test_teacher.py | 163 ++++ .../test_project/test_unauthenticated.py | 176 +++++ 13 files changed, 972 insertions(+), 955 deletions(-) delete mode 100644 backend/pigeonhole/tests/test_views/test_course.py create mode 100644 backend/pigeonhole/tests/test_views/test_course/__init__.py create mode 100644 backend/pigeonhole/tests/test_views/test_course/course.txt create mode 100644 backend/pigeonhole/tests/test_views/test_course/test_admin.py create mode 100644 backend/pigeonhole/tests/test_views/test_course/test_teacher.py delete mode 100644 backend/pigeonhole/tests/test_views/test_project.py create mode 100644 backend/pigeonhole/tests/test_views/test_project/__init__.py create mode 100644 backend/pigeonhole/tests/test_views/test_project/test_admin.py create mode 100644 backend/pigeonhole/tests/test_views/test_project/test_student.py create mode 100644 backend/pigeonhole/tests/test_views/test_project/test_teacher.py create mode 100644 backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py index 2fe552a8..0361f12a 100644 --- a/backend/pigeonhole/apps/courses/permissions.py +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -6,6 +6,9 @@ class CourseUserPermissions(permissions.BasePermission): def has_permission(self, request, view): if request.user.is_admin or request.user.is_superuser: return True + + if request.user.is_teacher: + return True if request.user.is_student or request.user.is_teacher: return view.action in ['list', 'retrieve'] diff --git a/backend/pigeonhole/apps/projects/permissions.py b/backend/pigeonhole/apps/projects/permissions.py index 6a1157ed..c350e5e5 100644 --- a/backend/pigeonhole/apps/projects/permissions.py +++ b/backend/pigeonhole/apps/projects/permissions.py @@ -14,5 +14,5 @@ def has_permission(self, request, view): return True elif user.is_student: if user.course.filter(course_id=course_id).exists(): - return True + return view.action in ['list', 'retrieve'] return False diff --git a/backend/pigeonhole/tests/test_views/test_course.py b/backend/pigeonhole/tests/test_views/test_course.py deleted file mode 100644 index 984f508a..00000000 --- a/backend/pigeonhole/tests/test_views/test_course.py +++ /dev/null @@ -1,245 +0,0 @@ -from django.test import TestCase -from rest_framework.test import APIClient -from rest_framework import status -from backend.pigeonhole.apps.users.models import User, Teacher, Student -from backend.pigeonhole.apps.courses.models import Course - -API_ENDPOINT = '/courses/' # Updated the API_ENDPOINT - - -class CourseTestAdminTeacher(TestCase): - def setUp(self): - self.client = APIClient() - - self.course_data = { - 'name': 'Test Course', - 'description': 'This is a test course.' - } - - self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", - description="This is not of the teacher") - - self.course = Course.objects.create(**self.course_data) - - # Create a regular user (teacher) - self.user = User.objects.create_user( - username="teacher_username", - email="test@gmail.com", - first_name="Kermit", - last_name="The Frog", - ) - - # Create a Teacher instance and use .set() to assign the course - self.teacher = Teacher.objects.create(id=self.user, is_admin=True) - self.teacher.course.set([self.course]) - - self.client.force_authenticate(user=self.user) - - def test_create_course(self): - response = self.client.post(API_ENDPOINT, self.course_data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Course.objects.count(), 3) - - def test_update_course(self): - updated_data = { - 'name': 'Updated Course', - 'description': 'This course has been updated.' - } - response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.course.refresh_from_db() - self.assertEqual(self.course.name, updated_data['name']) - self.assertEqual(self.course.description, updated_data['description']) - - def test_update_course_not_of_teacher(self): - updated_data = { - 'name': 'Updated Course', - 'description': 'This course has been updated.' - } - response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, - format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.course_not_of_teacher.refresh_from_db() - self.assertEqual(self.course_not_of_teacher.name, updated_data['name']) - self.assertEqual(self.course_not_of_teacher.description, updated_data['description']) - - def test_delete_course(self): - response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Course.objects.count(), 1) - - def test_delete_course_not_of_teacher(self): - response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Course.objects.count(), 1) - - def test_list_courses(self): - response = self.client.get(API_ENDPOINT) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), Course.objects.count()) - - -class CourseTestTeacher(TestCase): - def setUp(self): - self.client = APIClient() - - self.course_data = { - 'name': 'Test Course', - 'description': 'This is a test course.' - } - - self.course = Course.objects.create(**self.course_data) - - self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", - description="This is not of the teacher") - - # Create a regular user (teacher) - self.user = User.objects.create_user( - username="teacher_username", - email="teacher@gmail.com", - first_name="Kermit", - last_name="The Frog", - ) - - # Create a Teacher instance and use .set() to assign the course - self.teacher = Teacher.objects.create(id=self.user) - self.teacher.course.set([self.course]) - - # Authenticate the test client with the teacher user - self.client.force_authenticate(user=self.user) - - def test_create_course(self): - response = self.client.post(API_ENDPOINT, self.course_data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Course.objects.count(), 3) - - def test_update_course(self): - updated_data = { - 'name': 'Updated Course', - 'description': 'This course has been updated.' - } - response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.course.refresh_from_db() - self.assertEqual(self.course.name, updated_data['name']) - self.assertEqual(self.course.description, updated_data['description']) - - def test_update_course_not_of_teacher(self): - updated_data = { - 'name': 'Updated Course', - 'description': 'This course has been updated.' - } - response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, - format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_course(self): - response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Course.objects.count(), 1) - - def test_delete_course_not_of_teacher(self): - response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_courses(self): - response = self.client.get(API_ENDPOINT) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), Course.objects.count()) - - def test_retrieve_course(self): - response = self.client.get(f'{API_ENDPOINT}{self.course.course_id}/') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['name'], self.course.name) - self.assertEqual(response.data['description'], self.course.description) - - -class CourseTestStudent(TestCase): - def setUp(self): - self.client = APIClient() - - # Create a regular user (teacher) - self.user = User.objects.create_user( - username="teacher_username", - email="kermit@gmail.com", - first_name="Kermit", - last_name="The Frog" - ) - - self.course_data = { - 'name': 'Test Course', - 'description': 'This is a test course.' - } - - self.course = Course.objects.create(**self.course_data) - - # Provide a value for the "number" field when creating the Student instance - self.student = Student.objects.create(id=self.user, number=123456) - self.student.course.set([self.course]) - - # Authenticate the test client with the regular user - self.client.force_authenticate(user=self.user) - - def test_create_course(self): - response = self.client.post(API_ENDPOINT, self.course_data, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Course.objects.count(), 1) - - def test_update_course(self): - updated_data = { - 'name': 'Updated Course', - 'description': 'This course has been updated.' - } - response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.course.refresh_from_db() - self.assertNotEqual(self.course.name, updated_data['name']) - self.assertNotEqual(self.course.description, updated_data['description']) - - def test_delete_course(self): - response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Course.objects.count(), 1) - - def test_list_courses(self): - response = self.client.get(API_ENDPOINT) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) - - -class CourseTestUnauthorized(TestCase): - def setUp(self): - self.client = APIClient() - - self.course_data = { - 'name': 'Test Course', - 'description': 'This is a test course.' - } - - self.course = Course.objects.create(**self.course_data) - - def test_create_course(self): - response = self.client.post(API_ENDPOINT, self.course_data, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Course.objects.count(), 1) - - def test_update_course(self): - updated_data = { - 'name': 'Updated Course', - 'description': 'This course has been updated.' - } - response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.course.refresh_from_db() - self.assertNotEqual(self.course.name, updated_data['name']) - self.assertNotEqual(self.course.description, updated_data['description']) - - def test_delete_course(self): - response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Course.objects.count(), 1) - - def test_list_courses(self): - response = self.client.get(API_ENDPOINT) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(len(response.data), Course.objects.count()) diff --git a/backend/pigeonhole/tests/test_views/test_course/__init__.py b/backend/pigeonhole/tests/test_views/test_course/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_course/course.txt b/backend/pigeonhole/tests/test_views/test_course/course.txt new file mode 100644 index 00000000..cfcdd187 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course/course.txt @@ -0,0 +1,91 @@ + +class CourseTestStudent(TestCase): + def setUp(self): + self.client = APIClient() + + # Create a regular user (teacher) + self.user = User.objects.create_user( + username="teacher_username", + email="kermit@gmail.com", + first_name="Kermit", + last_name="The Frog" + ) + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + # Provide a value for the "number" field when creating the Student instance + self.student = Student.objects.create(id=self.user, number=123456) + self.student.course.set([self.course]) + + # Authenticate the test client with the regular user + self.client.force_authenticate(user=self.user) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.course.refresh_from_db() + self.assertNotEqual(self.course.name, updated_data['name']) + self.assertNotEqual(self.course.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + + +class CourseTestUnauthorized(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.course.refresh_from_db() + self.assertNotEqual(self.course.name, updated_data['name']) + self.assertNotEqual(self.course.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(len(response.data), Course.objects.count()) +""" \ No newline at end of file diff --git a/backend/pigeonhole/tests/test_views/test_course/test_admin.py b/backend/pigeonhole/tests/test_views/test_course/test_admin.py new file mode 100644 index 00000000..53e66a39 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course/test_admin.py @@ -0,0 +1,77 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class CourseTestAdminTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", + description="This is not of the teacher") + + self.course = Course.objects.create(**self.course_data) + + # Create a regular user (teacher) + self.teacher = User.objects.create( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=1, + ) + self.teacher.course.set([self.course]) + self.client.force_authenticate(user=self.teacher) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 3) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course.refresh_from_db() + self.assertEqual(self.course.name, updated_data['name']) + self.assertEqual(self.course.description, updated_data['description']) + + def test_update_course_not_of_teacher(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, + format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course_not_of_teacher.refresh_from_db() + self.assertEqual(self.course_not_of_teacher.name, updated_data['name']) + self.assertEqual(self.course_not_of_teacher.description, updated_data['description']) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_delete_course_not_of_teacher(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), Course.objects.count()) diff --git a/backend/pigeonhole/tests/test_views/test_course/test_teacher.py b/backend/pigeonhole/tests/test_views/test_course/test_teacher.py new file mode 100644 index 00000000..dd99f6a3 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_course/test_teacher.py @@ -0,0 +1,82 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +# TODO + +class CourseTestTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.course_data = { + 'name': 'Test Course', + 'description': 'This is a test course.' + } + + self.course = Course.objects.create(**self.course_data) + + self.course_not_of_teacher = Course.objects.create(name="Not of Teacher", + description="This is not of the teacher") + + self.teacher = User.objects.create( + username="teacher_username", + email="teacher@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=2 + ) + + self.teacher.course.set([self.course]) + + self.client.force_authenticate(user=self.teacher) + + def test_create_course(self): + response = self.client.post(API_ENDPOINT, self.course_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 3) + + def test_update_course(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course.course_id}/', updated_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.course.refresh_from_db() + self.assertEqual(self.course.name, updated_data['name']) + self.assertEqual(self.course.description, updated_data['description']) + + def test_update_course_not_of_teacher(self): + updated_data = { + 'name': 'Updated Course', + 'description': 'This course has been updated.' + } + response = self.client.put(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/', updated_data, + format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_course(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Course.objects.count(), 1) + + def test_delete_course_not_of_teacher(self): + response = self.client.delete(f'{API_ENDPOINT}{self.course_not_of_teacher.course_id}/') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_courses(self): + response = self.client.get(API_ENDPOINT) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), Course.objects.count()) + + def test_retrieve_course(self): + response = self.client.get(f'{API_ENDPOINT}{self.course.course_id}/') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['name'], self.course.name) + self.assertEqual(response.data['description'], self.course.description) diff --git a/backend/pigeonhole/tests/test_views/test_project.py b/backend/pigeonhole/tests/test_views/test_project.py deleted file mode 100644 index 1cd2ddb4..00000000 --- a/backend/pigeonhole/tests/test_views/test_project.py +++ /dev/null @@ -1,709 +0,0 @@ -from django.test import TestCase -from rest_framework.test import APIClient -from rest_framework import status -from backend.pigeonhole.apps.users.models import User, Teacher, Student -from backend.pigeonhole.apps.courses.models import Course -from backend.pigeonhole.apps.projects.models import Project - -API_ENDPOINT = '/courses/' # Updated the API_ENDPOINT - - -class ProjectTestAdminTeacher(TestCase): - def setUp(self): - self.client = APIClient() - self.user = User.objects.create_user( - username="teacher_username", - email="test@gmail.com", - first_name="Kermit", - last_name="The Frog", - ) - - self.teacher = Teacher.objects.create( - id=self.user, - is_admin=True - ) - - self.course = Course.objects.create( - name="Test Course", - description="Test Course Description", - ) - - self.teacher.course.set([self.course]) - - self.project = Project.objects.create( - name="Test Project", - course_id=self.course - ) - - self.client.force_authenticate(self.user) - - def test_create_project(self): - response = self.client.post( - API_ENDPOINT + f'{self.course.course_id}/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Project.objects.count(), 2) - self.assertEqual(Project.objects.get(project_id=2).name, "Test Project 2") - - def test_retrieve_project(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('name'), self.project.name) - - def test_list_projects(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - def test_update_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") - - def test_delete_project(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Project.objects.count(), 0) - - def test_partial_update_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") - - # tests with an invalid course - - def test_create_project_invalid_course(self): - response = self.client.post( - API_ENDPOINT + f'100/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": 100 - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(Project.objects.count(), 1) - - """TODO - def test_update_project_invalid_course(self): - response = self.client.patch( - API_ENDPOINT + f'100/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": 100 - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - """ - - def test_delete_project_invalid_course(self): - response = self.client.delete( - API_ENDPOINT + f'100/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(Project.objects.count(), 1) - - """TODO - def test_partial_update_project_invalid_course(self): - response = self.client.patch( - API_ENDPOINT + f'100/projects/{self.project.project_id}/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - """ - - def test_retrieve_project_invalid_course(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_list_projects_invalid_course(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - # test with invalid project - - def test_retrieve_invalid_project(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_update_invalid_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/100/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_partial_update_invalid_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/100/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_delete_invalid_project(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - -class ProjectTestTeacher(TestCase): - def setUp(self): - self.client = APIClient() - self.user = User.objects.create_user( - username="teacher_username", - email="test@gmail.com", - first_name="Kermit", - last_name="The Frog", - ) - - self.teacher = Teacher.objects.create( - id=self.user, - is_admin=False - ) - - self.course = Course.objects.create( - name="Test Course", - description="Test Course Description", - ) - - self.course_not_of_teacher = Course.objects.create( - name="Test Course 2", - ) - - self.teacher.course.set([self.course]) - - self.project = Project.objects.create( - name="Test Project", - course_id=self.course - ) - - self.client.force_authenticate(self.user) - - def test_create_project(self): - response = self.client.post( - API_ENDPOINT + f'{self.course.course_id}/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # Updated assertion to use the correct project_id from the response - created_project_id = response.data.get('project_id') - self.assertEqual(Project.objects.get(project_id=created_project_id).name, "Test Project 2") - self.assertEqual(Project.objects.count(), 2) - - def test_retrieve_project(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('name'), self.project.name) - - def test_list_projects(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - def test_update_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") - - def test_delete_project(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Project.objects.count(), 0) - - def test_partial_update_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") - - # tests with an invalid course - - def test_create_project_invalid_course(self): - response = self.client.post( - API_ENDPOINT + f'100/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": 100 - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project_invalid_course(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_projects_invalid_course(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # tests with a course not of the teacher - - def test_create_project_course_not_of_teacher(self): - response = self.client.post( - API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": self.course_not_of_teacher.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project_course_not_of_teacher(self): - response = self.client.get( - API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_projects_course_not_of_teacher(self): - response = self.client.get( - API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # test with invalid project - - def test_retrieve_invalid_project(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - -class ProjectTestStudent(TestCase): - def setUp(self): - self.client = APIClient() - self.user = User.objects.create_user( - username="student_username", - email="test@gmail.com", - first_name="Kermit", - last_name="The Frog", - ) - - self.student = Student.objects.create( - id=self.user, - number=1234567890 - ) - - self.course = Course.objects.create( - name="Test Course", - description="Test Course Description", - ) - - self.course_not_of_student = Course.objects.create( - name="Test Course 2", - ) - - self.student.course.set([self.course]) - - self.project = Project.objects.create( - name="Test Project", - course_id=self.course - ) - - self.client.force_authenticate(self.user) - - def test_create_project(self): - response = self.client.post( - API_ENDPOINT + f'{self.course.course_id}/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('name'), self.project.name) - - def test_list_projects(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - def test_update_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - - def test_delete_project(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_partial_update_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - - # tests with an invalid course - - def test_create_project_invalid_course(self): - response = self.client.post( - API_ENDPOINT + f'100/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": 100 - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project_invalid_course(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_projects_invalid_course(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # tests with a course not of the student - - def test_create_project_course_not_of_student(self): - response = self.client.post( - API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": self.course_not_of_student.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project_course_not_of_student(self): - response = self.client.get( - API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_projects_course_not_of_student(self): - response = self.client.get( - API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_project_course_not_of_student(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course_not_of_student.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") - - # test with invalid project - - def test_retrieve_invalid_project(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_update_invalid_project(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/100/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_invalid_project(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -class ProjectTestUnauthenticated(TestCase): - def setUp(self): - self.client = APIClient() - self.user = User.objects.create_user( - username="student_username", - email="test@gmail.com", - first_name="Kermit", - last_name="The Frog", - ) - - self.student = Student.objects.create( - id=self.user, - number=1234567890 - ) - - self.course = Course.objects.create( - name="Test Course", - description="Test Course Description", - ) - - self.student.course.set([self.course]) - - self.project = Project.objects.create( - name="Test Project", - course_id=self.course - ) - - def test_create_project_unauthenticated(self): - response = self.client.post( - API_ENDPOINT + f'{self.course.course_id}/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project_unauthenticated(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_projects_unauthenticated(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_project_unauthenticated(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_project_unauthenticated(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_partial_update_project_unauthenticated(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # tests with an invalid course - def test_create_project_invalid_course_unauthenticated(self): - response = self.client.post( - API_ENDPOINT + f'100/projects/', - { - "name": "Test Project 2", - "description": "Test Project 2 Description", - "course_id": 100 - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(Project.objects.count(), 1) - - def test_retrieve_project_invalid_course_unauthenticated(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_list_projects_invalid_course_unauthenticated(self): - response = self.client.get( - API_ENDPOINT + f'100/projects/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_project_invalid_course_unauthenticated(self): - response = self.client.patch( - API_ENDPOINT + f'100/projects/{self.project.project_id}/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": 100 - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_project_invalid_course_unauthenticated(self): - response = self.client.delete( - API_ENDPOINT + f'100/projects/{self.project.project_id}/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_partial_update_project_invalid_course_unauthenticated(self): - response = self.client.patch( - API_ENDPOINT + f'100/projects/{self.project.project_id}/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # test with invalid project - - def test_retrieve_invalid_project_unauthenticated(self): - response = self.client.get( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_invalid_project_unauthenticated(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/100/', - { - "name": "Updated Test Project", - "description": "Updated Test Project Description", - "course_id": self.course.course_id - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_invalid_project_unauthenticated(self): - response = self.client.delete( - API_ENDPOINT + f'{self.course.course_id}/projects/100/' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_partial_update_invalid_project_unauthenticated(self): - response = self.client.patch( - API_ENDPOINT + f'{self.course.course_id}/projects/100/', - { - "name": "Updated Test Project" - }, - format='json' - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/backend/pigeonhole/tests/test_views/test_project/__init__.py b/backend/pigeonhole/tests/test_views/test_project/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_project/test_admin.py b/backend/pigeonhole/tests/test_views/test_project/test_admin.py new file mode 100644 index 00000000..474bd2fe --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_admin.py @@ -0,0 +1,189 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestAdminTeacher(TestCase): + def setUp(self): + self.client = APIClient() + + self.admin = User.objects.create( + username="admin_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=1 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.admin.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.admin) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Project.objects.count(), 2) + self.assertEqual(Project.objects.get(project_id=2).name, "Test Project 2") + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Project.objects.count(), 0) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.count(), 1) + + def test_update_project_invalid_course(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_delete_project_invalid_course(self): + response = self.client.delete( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.count(), 1) + + def test_partial_update_project_invalid_course(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_partial_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_student.py b/backend/pigeonhole/tests/test_views/test_project/test_student.py new file mode 100644 index 00000000..438f9ed5 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_student.py @@ -0,0 +1,190 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestStudent(TestCase): + def setUp(self): + self.client = APIClient() + self.student = User.objects.create( + username="student_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=3 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.course_not_of_student = Course.objects.create( + name="Test Course 2", + ) + + self.student.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.student) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with a course not of the student + + def test_create_project_course_not_of_student(self): + response = self.client.post( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course_not_of_student.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_course_not_of_student(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_course_not_of_student(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_course_not_of_student(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course_not_of_student.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course_not_of_student.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Test Project") + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_teacher.py b/backend/pigeonhole/tests/test_views/test_project/test_teacher.py new file mode 100644 index 00000000..f49a0559 --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_teacher.py @@ -0,0 +1,163 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestTeacher(TestCase): + def setUp(self): + self.client = APIClient() + self.teacher = User.objects.create( + username="teacher_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=2 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.course_not_of_teacher = Course.objects.create( + name="Test Course 2", + ) + + self.teacher.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + self.client.force_authenticate(self.teacher) + + def test_create_project(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Updated assertion to use the correct project_id from the response + created_project_id = response.data.get('project_id') + self.assertEqual(Project.objects.get(project_id=created_project_id).name, "Test Project 2") + self.assertEqual(Project.objects.count(), 2) + + def test_retrieve_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('name'), self.project.name) + + def test_list_projects(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + def test_delete_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Project.objects.count(), 0) + + def test_partial_update_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Project.objects.get(project_id=self.project.project_id).name, "Updated Test Project") + + # tests with an invalid course + + def test_create_project_invalid_course(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with a course not of the teacher + + def test_create_project_course_not_of_teacher(self): + response = self.client.post( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course_not_of_teacher.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_course_not_of_teacher(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_course_not_of_teacher(self): + response = self.client.get( + API_ENDPOINT + f'{self.course_not_of_teacher.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # test with invalid project + + def test_retrieve_invalid_project(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py b/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py new file mode 100644 index 00000000..f4e42afb --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py @@ -0,0 +1,176 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User + +API_ENDPOINT = '/courses/' + + +class ProjectTestUnauthenticated(TestCase): + def setUp(self): + self.client = APIClient() + self.student = User.objects.create( + username="student_username", + email="test@gmail.com", + first_name="Kermit", + last_name="The Frog", + role=3 + ) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + self.student.course.set([self.course]) + + self.project = Project.objects.create( + name="Test Project", + course_id=self.course + ) + + def test_create_project_unauthenticated(self): + response = self.client.post( + API_ENDPOINT + f'{self.course.course_id}/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_project_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # tests with an invalid course + def test_create_project_invalid_course_unauthenticated(self): + response = self.client.post( + API_ENDPOINT + f'100/projects/', + { + "name": "Test Project 2", + "description": "Test Project 2 Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(Project.objects.count(), 1) + + def test_retrieve_project_invalid_course_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_list_projects_invalid_course_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'100/projects/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_project_invalid_course_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": 100 + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_project_invalid_course_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'100/projects/{self.project.project_id}/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_project_invalid_course_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'100/projects/{self.project.project_id}/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # test with invalid project + + def test_retrieve_invalid_project_unauthenticated(self): + response = self.client.get( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_invalid_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_invalid_project_unauthenticated(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_partial_update_invalid_project_unauthenticated(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project" + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) From aa2a7b2a3dc1c796b8203f1275a6370280327e69 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Tue, 12 Mar 2024 09:20:51 +0100 Subject: [PATCH 49/58] todo's checks --- backend/pigeonhole/apps/groups/views.py | 2 ++ backend/pigeonhole/apps/projects/views.py | 1 + backend/pigeonhole/apps/submissions/views.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/backend/pigeonhole/apps/groups/views.py b/backend/pigeonhole/apps/groups/views.py index 5557f971..2ec62913 100644 --- a/backend/pigeonhole/apps/groups/views.py +++ b/backend/pigeonhole/apps/groups/views.py @@ -7,6 +7,8 @@ from backend.pigeonhole.apps.groups.models import Group, GroupSerializer from backend.pigeonhole.apps.projects.models import Project +# TODO tests for score/max_score + class GroupViewSet(viewsets.ModelViewSet): queryset = Group.objects.all() diff --git a/backend/pigeonhole/apps/projects/views.py b/backend/pigeonhole/apps/projects/views.py index a929c8d2..f1f19cf9 100644 --- a/backend/pigeonhole/apps/projects/views.py +++ b/backend/pigeonhole/apps/projects/views.py @@ -8,6 +8,7 @@ from .permissions import CanAccessProject # TODO hier nog zorgen als een project niet visible is, dat de students het niet kunnen zien. +# TODO tests for visibility and deadline class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() diff --git a/backend/pigeonhole/apps/submissions/views.py b/backend/pigeonhole/apps/submissions/views.py index 8a03e474..67a42403 100644 --- a/backend/pigeonhole/apps/submissions/views.py +++ b/backend/pigeonhole/apps/submissions/views.py @@ -4,6 +4,8 @@ from backend.pigeonhole.apps.submissions.models import Submissions, SubmissionsSerializer +# TODO test timestamp, file, output_test + class SubmissionsViewset(viewsets.ModelViewSet): queryset = Submissions.objects.all() From 3376aa232b27a764d6efb6a64ec7b2033308c2b6 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Tue, 12 Mar 2024 09:43:27 +0100 Subject: [PATCH 50/58] clean testen --- .../tests/test_views/test_complete/admin.py | 90 +++++++++++++++++++ .../tests/test_views/test_group/__init__.py | 0 .../test_views/test_project/test_teacher.py | 18 ++++ .../test_project/test_unauthenticated.py | 1 + .../test_views/test_submission/__init__.py | 0 5 files changed, 109 insertions(+) create mode 100644 backend/pigeonhole/tests/test_views/test_complete/admin.py create mode 100644 backend/pigeonhole/tests/test_views/test_group/__init__.py create mode 100644 backend/pigeonhole/tests/test_views/test_submission/__init__.py diff --git a/backend/pigeonhole/tests/test_views/test_complete/admin.py b/backend/pigeonhole/tests/test_views/test_complete/admin.py new file mode 100644 index 00000000..da4d7afd --- /dev/null +++ b/backend/pigeonhole/tests/test_views/test_complete/admin.py @@ -0,0 +1,90 @@ +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient + +from backend.pigeonhole.apps.courses.models import Course +from backend.pigeonhole.apps.projects.models import Project +from backend.pigeonhole.apps.users.models import User +from backend.pigeonhole.apps.submissions.models import Submission + +ROUTES_PREFIX = '/courses/' + + +class CompleteTestAdmin(TestCase): + def setUp(self): + self.client = APIClient() + + # Create a teacher user + self.teacher = User.objects.create( + username="teacher_username", + email="teacher@gmail.com", + first_name="Teacher", + last_name="LastName", + role=2 # Teacher role + ) + + # Create a student user + self.student = User.objects.create( + username="student_username", + email="student@gmail.com", + first_name="Student", + last_name="LastName", + role=3 # Student role + ) + + # Authenticate the teacher user + self.client.force_authenticate(self.teacher) + + self.course = Course.objects.create( + name="Test Course", + description="Test Course Description", + ) + + def test_create_course(self): + # Use the teacher to create the course + response = self.client.post( + ROUTES_PREFIX, + { + "name": "Test Course 2", + "description": "Test Course 2 Description", + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Course.objects.count(), 2) + self.assertEqual(Course.objects.get(course_id=2).name, "Test Course 2") + + def test_create_project(self): + # Use the teacher to create the project for the course + response = self.client.post( + ROUTES_PREFIX + f'{self.course.course_id}/projects/', + { + "name": "Test Project", + "description": "Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Project.objects.count(), 1) + self.assertEqual(Project.objects.get(project_id=1).name, "Test Project") + + def test_create_submission(self): + # Authenticate the student user + self.client.force_authenticate(self.student) + + # Use the student to create the submission for the project + response = self.client.post( + ROUTES_PREFIX + f'{self.course.course_id}/projects/1/submissions/', + { + "project_id": 1, + "student_id": self.student.id, # Use the student's id + "submission": "Test Submission", + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Submission.objects.count(), 1) + self.assertEqual(Submission.objects.get(submission_id=1).submission, "Test Submission") + + \ No newline at end of file diff --git a/backend/pigeonhole/tests/test_views/test_group/__init__.py b/backend/pigeonhole/tests/test_views/test_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/pigeonhole/tests/test_views/test_project/test_teacher.py b/backend/pigeonhole/tests/test_views/test_project/test_teacher.py index f49a0559..c2e565f7 100644 --- a/backend/pigeonhole/tests/test_views/test_project/test_teacher.py +++ b/backend/pigeonhole/tests/test_views/test_project/test_teacher.py @@ -161,3 +161,21 @@ def test_retrieve_invalid_project(self): API_ENDPOINT + f'{self.course.course_id}/projects/100/' ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_update_project_invalid_project(self): + response = self.client.patch( + API_ENDPOINT + f'{self.course.course_id}/projects/100/', + { + "name": "Updated Test Project", + "description": "Updated Test Project Description", + "course_id": self.course.course_id + }, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_project_invalid_project(self): + response = self.client.delete( + API_ENDPOINT + f'{self.course.course_id}/projects/100/' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py b/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py index f4e42afb..4d54ae51 100644 --- a/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py +++ b/backend/pigeonhole/tests/test_views/test_project/test_unauthenticated.py @@ -86,6 +86,7 @@ def test_partial_update_project_unauthenticated(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) # tests with an invalid course + def test_create_project_invalid_course_unauthenticated(self): response = self.client.post( API_ENDPOINT + f'100/projects/', diff --git a/backend/pigeonhole/tests/test_views/test_submission/__init__.py b/backend/pigeonhole/tests/test_views/test_submission/__init__.py new file mode 100644 index 00000000..e69de29b From 4e82db65f77982f0e542013dcff623a8fc18778e Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Tue, 12 Mar 2024 11:58:13 +0100 Subject: [PATCH 51/58] add basic fronted authentication --- backend/pigeonhole/urls.py | 9 +- frontend/package-lock.json | 161 ++++++++++++++++++++- frontend/package.json | 2 + frontend/src/app/components/LoginForm.tsx | 16 +- frontend/src/authentication/auth-header.js | 10 ++ frontend/src/authentication/auth.js | 28 ++++ frontend/src/proxy.js | 11 ++ 7 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 frontend/src/authentication/auth-header.js create mode 100644 frontend/src/authentication/auth.js create mode 100644 frontend/src/proxy.js diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 883e1350..05350b0b 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,21 +1,22 @@ from django.contrib import admin from django.urls import include, path from rest_framework import routers +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from backend.pigeonhole.apps.users.viewsets import LoginViewSet from backend.testapi import views router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'groups', views.GroupViewSet) -router.register(r'login', LoginViewSet, basename='auth-login') # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), - path('admin/', admin.site.urls) + path('api-authentication/', include('rest_framework.urls', namespace='rest_framework')), + path('admin/', admin.site.urls), + path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('refresh/', TokenRefreshView.as_view(), name='token_refresh'), ] urlpatterns += router.urls diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 92e63922..4aee891d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@radix-ui/react-slot": "^1.0.2", + "axios": "^1.6.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "lucide-react": "^0.344.0", @@ -28,6 +29,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "http-proxy-middleware": "^2.0.6", "eslint": "^8", "eslint-config-next": "14.1.0", "postcss": "^8", @@ -1107,6 +1109,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1117,7 +1127,6 @@ "version": "20.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1561,6 +1570,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.17", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", @@ -1622,6 +1636,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -1874,6 +1898,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2008,6 +2043,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2675,6 +2718,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2791,6 +2839,25 @@ "integrity": "sha512-noqGuLw158+DuD9UPRKHpJ2hGxpFyDlYYrfM0mWt4XhT4n0lwzTLh70Tkdyy4kyTmyTT9Bv7bWAJqw7cgkEXDg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2815,6 +2882,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3145,6 +3225,42 @@ "react-is": "^16.7.0" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3428,6 +3544,17 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3781,6 +3908,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4409,6 +4555,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4543,6 +4694,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5329,8 +5485,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/update-browserslist-db": { "version": "1.0.13", diff --git a/frontend/package.json b/frontend/package.json index 81adcf9f..9d6b7c12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,8 +15,10 @@ "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@radix-ui/react-slot": "^1.0.2", + "axios": "^1.6.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "http-proxy-middleware": "^2.0.6", "lucide-react": "^0.344.0", "next": "14.1.0", "react": "^18", diff --git a/frontend/src/app/components/LoginForm.tsx b/frontend/src/app/components/LoginForm.tsx index abb5f37c..de1db578 100644 --- a/frontend/src/app/components/LoginForm.tsx +++ b/frontend/src/app/components/LoginForm.tsx @@ -3,15 +3,19 @@ import React, {useState} from 'react'; import {Button, IconButton, InputAdornment, TextField} from "@mui/material"; import Visibility from "@mui/icons-material/Visibility"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import AuthService from "../../authentication/auth"; const LoginForm = () => { - const [email, setEmail] = useState(''); + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const handleLogin = (): void => { // Implement your login logic here - console.log('Login with:', email, password); + AuthService.login(username, password).then((data) => { + console.log("Logged in") + console.log(data) + }) }; const handleClickShowPassword = () => { @@ -25,10 +29,10 @@ const LoginForm = () => { return (
setEmail(e.target.value)} + label="Username" + autoComplete="username" + value={username} + onChange={(e) => setUsername(e.target.value)} fullWidth /> { + console.log(response) + if (response.data.access) { + localStorage.setItem("user", JSON.stringify(response.data.user)); + } + return response.data; + }); + } + + getCurrentUser() { + return JSON.parse(localStorage.getItem('user')); + } + +} + +export default new Auth(); \ No newline at end of file diff --git a/frontend/src/proxy.js b/frontend/src/proxy.js new file mode 100644 index 00000000..52198015 --- /dev/null +++ b/frontend/src/proxy.js @@ -0,0 +1,11 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = function(app) { + app.use( + '/api', + createProxyMiddleware({ + target: 'http://localhost:8000/', + changeOrigin: true, + }) + ); +}; \ No newline at end of file From b32c57dd3d64d1b0ff9b7f5a23aa79c79c8cf8f7 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Tue, 12 Mar 2024 11:58:31 +0100 Subject: [PATCH 52/58] docker make tools --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 01c1abef..6702dd20 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,11 @@ stop: lint: docker exec pigeonhole-backend flake8 . - docker exec pigeonhole-frontend npm run lint \ No newline at end of file + docker exec pigeonhole-frontend npm run lint + +superuser: + docker exec -it pigeonhole-backend python manage.py createsuperuser + +reset: + docker image prune -af + docker system prune \ No newline at end of file From 7f1d4df764674e040d4fd61446baaf0b32f3bea9 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Tue, 12 Mar 2024 13:13:50 +0100 Subject: [PATCH 53/58] cleanup serializer --- backend/pigeonhole/apps/users/models.py | 7 ------- backend/pigeonhole/apps/users/serializers.py | 16 ++-------------- backend/pigeonhole/apps/users/views.py | 3 ++- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index 5f6d02c9..f8224677 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -1,6 +1,5 @@ from django.contrib.auth.models import AbstractUser from django.db import models -from rest_framework import serializers from backend.pigeonhole.apps.courses.models import Course @@ -39,9 +38,3 @@ def is_teacher(self): @property def is_student(self): return self.role == Roles.STUDENT - - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ['id', 'email', 'first_name', 'last_name', 'course', 'role'] diff --git a/backend/pigeonhole/apps/users/serializers.py b/backend/pigeonhole/apps/users/serializers.py index 2856028d..968c163e 100644 --- a/backend/pigeonhole/apps/users/serializers.py +++ b/backend/pigeonhole/apps/users/serializers.py @@ -3,25 +3,13 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.settings import api_settings -from backend.pigeonhole.apps.users.models import User, Student, Teacher +from backend.pigeonhole.apps.users.models import User class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'e_mail', 'first_name', 'last_name'] - - -class StudentSerializer(serializers.ModelSerializer): - class Meta: - model = Student - fields = ['number', 'course', 'id'] - - -class TeacherSerializer(serializers.ModelSerializer): - class Meta: - model = Teacher - fields = ['course', 'id', 'is_admin', 'is_assistent'] + fields = ['id', 'email', 'first_name', 'last_name', 'course', 'role'] class LoginSerializer(TokenObtainPairSerializer): diff --git a/backend/pigeonhole/apps/users/views.py b/backend/pigeonhole/apps/users/views.py index 5fec4879..1aa9dab5 100644 --- a/backend/pigeonhole/apps/users/views.py +++ b/backend/pigeonhole/apps/users/views.py @@ -2,7 +2,8 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from backend.pigeonhole.apps.users.models import User, UserSerializer +from backend.pigeonhole.apps.users.models import User +from backend.pigeonhole.apps.users.serializers import UserSerializer class UserViewSet(viewsets.ModelViewSet): From bab63ca6e862c8bf795062fa6b8f818de103634f Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Tue, 12 Mar 2024 14:06:16 +0100 Subject: [PATCH 54/58] fix user manager --- backend/pigeonhole/apps/users/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/pigeonhole/apps/users/models.py b/backend/pigeonhole/apps/users/models.py index f8224677..029a3f6f 100644 --- a/backend/pigeonhole/apps/users/models.py +++ b/backend/pigeonhole/apps/users/models.py @@ -1,4 +1,4 @@ -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, UserManager from django.db import models from backend.pigeonhole.apps.courses.models import Course @@ -12,13 +12,10 @@ class Roles(models.IntegerChoices): class User(AbstractUser): id = models.BigAutoField(primary_key=True) - email = models.EmailField(unique=True) - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=150) course = models.ManyToManyField(Course) role = models.IntegerField(choices=Roles.choices, default=Roles.STUDENT) - objects = models.Manager() + objects = UserManager() class Meta(AbstractUser.Meta): db_table = "auth_user" From 2de5362293d3074c5e1e0c14a075bf4419893681 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Tue, 12 Mar 2024 14:06:52 +0100 Subject: [PATCH 55/58] cleanup --- backend/pigeonhole/urls.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/pigeonhole/urls.py b/backend/pigeonhole/urls.py index 75f4428b..7e1c4468 100644 --- a/backend/pigeonhole/urls.py +++ b/backend/pigeonhole/urls.py @@ -1,8 +1,3 @@ -from backend.pigeonhole.apps.courses.views import CourseViewSet -from backend.pigeonhole.apps.groups.views import GroupViewSet -from backend.pigeonhole.apps.projects.views import ProjectViewSet -from backend.pigeonhole.apps.submissions.views import SubmissionsViewset -from backend.pigeonhole.apps.users.views import UserViewSet from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -12,6 +7,12 @@ from rest_framework import routers, permissions from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from backend.pigeonhole.apps.courses.views import CourseViewSet +from backend.pigeonhole.apps.groups.views import GroupViewSet +from backend.pigeonhole.apps.projects.views import ProjectViewSet +from backend.pigeonhole.apps.submissions.views import SubmissionsViewset +from backend.pigeonhole.apps.users.views import UserViewSet + schema_view = get_schema_view( openapi.Info( title="My API", From a36f155a6715c163a368846119bb9bb7b342dd97 Mon Sep 17 00:00:00 2001 From: Axel Lorreyne Date: Tue, 12 Mar 2024 14:07:09 +0100 Subject: [PATCH 56/58] reset migrations --- .../apps/courses/migrations/0001_initial.py | 22 ----- .../apps/groups/migrations/0001_initial.py | 26 ------ .../apps/groups/migrations/0002_initial.py | 21 ----- .../migrations/0003_alter_group_group_nr.py | 18 ---- .../0004_alter_group_final_score.py | 18 ---- .../0005_remove_group_student_group_user.py | 24 ------ .../apps/projects/migrations/0001_initial.py | 52 ------------ .../migrations/0002_project_deadline.py | 18 ---- .../migrations/0003_alter_project_deadline.py | 18 ---- .../migrations/0004_alter_project_deadline.py | 18 ---- .../submissions/migrations/0001_initial.py | 29 ------- ...ions_file_alter_submissions_output_test.py | 26 ------ ...ions_file_alter_submissions_output_test.py | 30 ------- ...ions_file_alter_submissions_output_test.py | 32 ------- .../apps/users/migrations/0001_initial.py | 83 ------------------- ...ename_is_assistent_teacher_is_assistant.py | 18 ---- ...acher_course_remove_teacher_id_and_more.py | 64 -------------- 17 files changed, 517 deletions(-) delete mode 100644 backend/pigeonhole/apps/courses/migrations/0001_initial.py delete mode 100644 backend/pigeonhole/apps/groups/migrations/0001_initial.py delete mode 100644 backend/pigeonhole/apps/groups/migrations/0002_initial.py delete mode 100644 backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py delete mode 100644 backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py delete mode 100644 backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py delete mode 100644 backend/pigeonhole/apps/projects/migrations/0001_initial.py delete mode 100644 backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py delete mode 100644 backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py delete mode 100644 backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py delete mode 100644 backend/pigeonhole/apps/submissions/migrations/0001_initial.py delete mode 100644 backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py delete mode 100644 backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py delete mode 100644 backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py delete mode 100644 backend/pigeonhole/apps/users/migrations/0001_initial.py delete mode 100644 backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py delete mode 100644 backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py diff --git a/backend/pigeonhole/apps/courses/migrations/0001_initial.py b/backend/pigeonhole/apps/courses/migrations/0001_initial.py deleted file mode 100644 index 48a29d53..00000000 --- a/backend/pigeonhole/apps/courses/migrations/0001_initial.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Course', - fields=[ - ('course_id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=256)), - ('description', models.TextField()), - ], - ), - ] diff --git a/backend/pigeonhole/apps/groups/migrations/0001_initial.py b/backend/pigeonhole/apps/groups/migrations/0001_initial.py deleted file mode 100644 index 1102b309..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0001_initial.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:00 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('projects', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Group', - fields=[ - ('group_id', models.BigAutoField(primary_key=True, serialize=False)), - ('group_nr', models.IntegerField()), - ('feedback', models.TextField(null=True)), - ('final_score', models.IntegerField()), - ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), - ], - ), - ] diff --git a/backend/pigeonhole/apps/groups/migrations/0002_initial.py b/backend/pigeonhole/apps/groups/migrations/0002_initial.py deleted file mode 100644 index 649b27b5..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0002_initial.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('groups', '0001_initial'), - ('users', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='group', - name='student', - field=models.ManyToManyField(to='users.student'), - ), - ] diff --git a/backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py b/backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py deleted file mode 100644 index 51d6180a..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0003_alter_group_group_nr.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 20:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('groups', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='group', - name='group_nr', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py b/backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py deleted file mode 100644 index 1a72c57e..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0004_alter_group_final_score.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 20:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('groups', '0003_alter_group_group_nr'), - ] - - operations = [ - migrations.AlterField( - model_name='group', - name='final_score', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py b/backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py deleted file mode 100644 index 34f5581e..00000000 --- a/backend/pigeonhole/apps/groups/migrations/0005_remove_group_student_group_user.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-11 13:17 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('groups', '0004_alter_group_final_score'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='group', - name='student', - ), - migrations.AddField( - model_name='group', - name='user', - field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/pigeonhole/apps/projects/migrations/0001_initial.py b/backend/pigeonhole/apps/projects/migrations/0001_initial.py deleted file mode 100644 index d849fb34..00000000 --- a/backend/pigeonhole/apps/projects/migrations/0001_initial.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:00 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ('courses', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Project', - fields=[ - ('project_id', models.BigAutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=256)), - ('description', models.TextField()), - ('visible', models.BooleanField(default=False)), - ('course_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.course')), - ], - ), - migrations.CreateModel( - name='ForbiddenExtension', - fields=[ - ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), - ('extension', models.CharField(max_length=512)), - ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), - ], - ), - migrations.CreateModel( - name='Conditions', - fields=[ - ('condition_id', models.BigAutoField(primary_key=True, serialize=False)), - ('condition', models.TextField(max_length=256)), - ('test_file_location', models.CharField(max_length=512, null=True)), - ('test_file_type', models.CharField(max_length=256, null=True)), - ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - to='projects.project')), - ], - ), - migrations.CreateModel( - name='AllowedExtension', - fields=[ - ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), - ('extension', models.CharField(max_length=512)), - ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), - ], - ), - ] diff --git a/backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py b/backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py deleted file mode 100644 index 330da245..00000000 --- a/backend/pigeonhole/apps/projects/migrations/0002_project_deadline.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='project', - name='deadline', - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py b/backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py deleted file mode 100644 index 639d1bc8..00000000 --- a/backend/pigeonhole/apps/projects/migrations/0003_alter_project_deadline.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0002_project_deadline'), - ] - - operations = [ - migrations.AlterField( - model_name='project', - name='deadline', - field=models.DateTimeField(null=True), - ), - ] diff --git a/backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py b/backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py deleted file mode 100644 index f9b12a86..00000000 --- a/backend/pigeonhole/apps/projects/migrations/0004_alter_project_deadline.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0003_alter_project_deadline'), - ] - - operations = [ - migrations.AlterField( - model_name='project', - name='deadline', - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py deleted file mode 100644 index 715328f3..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:00 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ('groups', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Submissions', - fields=[ - ('submission_id', models.BigAutoField(primary_key=True, serialize=False)), - ('submission_nr', models.IntegerField()), - ('file', models.FileField(null=True, upload_to='uploads//' - '/')), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('output_test', models.FileField(null=True, upload_to='uploads///output_test/')), - ('group_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='groups.group')), - ], - ), - ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index 56752ac0..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0002_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-02 21:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('submissions', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField(null=True, - upload_to='uploads///'), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField(null=True, - upload_to='uploads///output_test/'), - ), - ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index 48eb7b6e..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0003_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-05 11:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('submissions', - '0002_alter_submissions_file_alter_submissions_output_test'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField(max_length=255, null=True, - upload_to='uploads///',), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField(max_length=255, null=True, - upload_to='uploads///output_test/',), - ), - ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py b/backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py deleted file mode 100644 index c2aa722a..00000000 --- a/backend/pigeonhole/apps/submissions/migrations/0004_alter_submissions_file_alter_submissions_output_test.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 20:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('submissions', - '0003_alter_submissions_file_alter_submissions_output_test'), - ] - - operations = [ - migrations.AlterField( - model_name='submissions', - name='file', - field=models.FileField( - max_length=255, - null=True, - upload_to='uploads/submissions/files///'), - ), - migrations.AlterField( - model_name='submissions', - name='output_test', - field=models.FileField( - max_length=255, - null=True, - upload_to='uploads/submissions/outputs//' - '/output_test/'), - ), - ] diff --git a/backend/pigeonhole/apps/users/migrations/0001_initial.py b/backend/pigeonhole/apps/users/migrations/0001_initial.py deleted file mode 100644 index 36858182..00000000 --- a/backend/pigeonhole/apps/users/migrations/0001_initial.py +++ /dev/null @@ -1,83 +0,0 @@ -# Generated by Django 5.0.3 on 2024-03-10 16:00 - -import django.contrib.auth.models -import django.contrib.auth.validators -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('courses', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has ' - 'all permissions without explicitly ' - 'assigning them.', - verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, - help_text='Required. 150 characters or fewer. Letters, digits and ' - '@/./+/-/_ only.', max_length=150, unique=True, - validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], - verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into ' - 'this admin site.', - verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be ' - 'treated as active. Unselect this instead ' - 'of deleting accounts.', - verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will ' - 'get all permissions granted to each of their ' - 'groups.', related_name='user_set', - related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', - related_name='user_set', related_query_name='user', - to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'db_table': 'auth_user', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='Student', - fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, - to=settings.AUTH_USER_MODEL)), - ('number', models.IntegerField()), - ('course', models.ManyToManyField(to='courses.course')), - ], - ), - migrations.CreateModel( - name='Teacher', - fields=[ - ('id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, - to=settings.AUTH_USER_MODEL)), - ('is_admin', models.BooleanField(default=False)), - ('is_assistent', models.BooleanField(default=False)), - ('course', models.ManyToManyField(to='courses.course')), - ], - ), - ] diff --git a/backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py b/backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py deleted file mode 100644 index a9cf87be..00000000 --- a/backend/pigeonhole/apps/users/migrations/0002_rename_is_assistent_teacher_is_assistant.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-07 18:53 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='teacher', - old_name='is_assistent', - new_name='is_assistant', - ), - ] diff --git a/backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py b/backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py deleted file mode 100644 index 67dd6291..00000000 --- a/backend/pigeonhole/apps/users/migrations/0003_remove_teacher_course_remove_teacher_id_and_more.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 5.0.2 on 2024-03-11 13:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('courses', '0001_initial'), - ('groups', '0005_remove_group_student_group_user'), - ('users', '0002_rename_is_assistent_teacher_is_assistant'), - ] - - operations = [ - migrations.RemoveField( - model_name='teacher', - name='course', - ), - migrations.RemoveField( - model_name='teacher', - name='id', - ), - migrations.AlterModelManagers( - name='user', - managers=[ - ], - ), - migrations.AddField( - model_name='user', - name='course', - field=models.ManyToManyField(to='courses.course'), - ), - migrations.AddField( - model_name='user', - name='role', - field=models.IntegerField(choices=[(1, 'Superuser'), (2, 'Teacher'), (3, 'Student')], default=3), - ), - migrations.AlterField( - model_name='user', - name='email', - field=models.EmailField(max_length=254, unique=True), - ), - migrations.AlterField( - model_name='user', - name='first_name', - field=models.CharField(max_length=30), - ), - migrations.AlterField( - model_name='user', - name='id', - field=models.BigAutoField(primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='user', - name='last_name', - field=models.CharField(max_length=150), - ), - migrations.DeleteModel( - name='Student', - ), - migrations.DeleteModel( - name='Teacher', - ), - ] From 871e91a1fd47d467a0370570ad852849ae2cf956 Mon Sep 17 00:00:00 2001 From: PJDeSmijter Date: Tue, 12 Mar 2024 14:31:47 +0100 Subject: [PATCH 57/58] migrations axel --- .../apps/courses/migrations/0001_initial.py | 22 ++++++++ .../apps/groups/migrations/0001_initial.py | 26 +++++++++ .../apps/groups/migrations/0002_initial.py | 22 ++++++++ .../apps/projects/migrations/0001_initial.py | 53 +++++++++++++++++++ .../submissions/migrations/0001_initial.py | 27 ++++++++++ .../apps/users/migrations/0001_initial.py | 48 +++++++++++++++++ 6 files changed, 198 insertions(+) create mode 100644 backend/pigeonhole/apps/courses/migrations/0001_initial.py create mode 100644 backend/pigeonhole/apps/groups/migrations/0001_initial.py create mode 100644 backend/pigeonhole/apps/groups/migrations/0002_initial.py create mode 100644 backend/pigeonhole/apps/projects/migrations/0001_initial.py create mode 100644 backend/pigeonhole/apps/submissions/migrations/0001_initial.py create mode 100644 backend/pigeonhole/apps/users/migrations/0001_initial.py diff --git a/backend/pigeonhole/apps/courses/migrations/0001_initial.py b/backend/pigeonhole/apps/courses/migrations/0001_initial.py new file mode 100644 index 00000000..2df87f49 --- /dev/null +++ b/backend/pigeonhole/apps/courses/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-03-12 13:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Course', + fields=[ + ('course_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=256)), + ('description', models.TextField()), + ], + ), + ] diff --git a/backend/pigeonhole/apps/groups/migrations/0001_initial.py b/backend/pigeonhole/apps/groups/migrations/0001_initial.py new file mode 100644 index 00000000..b2685687 --- /dev/null +++ b/backend/pigeonhole/apps/groups/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.3 on 2024-03-12 13:29 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('projects', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('group_id', models.BigAutoField(primary_key=True, serialize=False)), + ('group_nr', models.IntegerField(blank=True, null=True)), + ('feedback', models.TextField(null=True)), + ('final_score', models.IntegerField(blank=True, null=True)), + ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), + ], + ), + ] diff --git a/backend/pigeonhole/apps/groups/migrations/0002_initial.py b/backend/pigeonhole/apps/groups/migrations/0002_initial.py new file mode 100644 index 00000000..61ffe259 --- /dev/null +++ b/backend/pigeonhole/apps/groups/migrations/0002_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-03-12 13:29 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('groups', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='user', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/pigeonhole/apps/projects/migrations/0001_initial.py b/backend/pigeonhole/apps/projects/migrations/0001_initial.py new file mode 100644 index 00000000..3101940e --- /dev/null +++ b/backend/pigeonhole/apps/projects/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 5.0.3 on 2024-03-12 13:29 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('courses', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Project', + fields=[ + ('project_id', models.BigAutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=256)), + ('description', models.TextField()), + ('deadline', models.DateTimeField(blank=True, null=True)), + ('visible', models.BooleanField(default=False)), + ('course_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.course')), + ], + ), + migrations.CreateModel( + name='ForbiddenExtension', + fields=[ + ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), + ('extension', models.CharField(max_length=512)), + ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), + ], + ), + migrations.CreateModel( + name='Conditions', + fields=[ + ('condition_id', models.BigAutoField(primary_key=True, serialize=False)), + ('condition', models.TextField(max_length=256)), + ('test_file_location', models.CharField(max_length=512, null=True)), + ('test_file_type', models.CharField(max_length=256, null=True)), + ('submission_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), + ], + ), + migrations.CreateModel( + name='AllowedExtension', + fields=[ + ('extension_id', models.BigAutoField(primary_key=True, serialize=False)), + ('extension', models.CharField(max_length=512)), + ('project_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.project')), + ], + ), + ] diff --git a/backend/pigeonhole/apps/submissions/migrations/0001_initial.py b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py new file mode 100644 index 00000000..4c3e9dd1 --- /dev/null +++ b/backend/pigeonhole/apps/submissions/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.3 on 2024-03-12 13:29 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('groups', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Submissions', + fields=[ + ('submission_id', models.BigAutoField(primary_key=True, serialize=False)), + ('submission_nr', models.IntegerField()), + ('file', models.FileField(max_length=255, null=True, upload_to='uploads/submissions/files///')), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('output_test', models.FileField(max_length=255, null=True, upload_to='uploads/submissions/outputs///output_test/')), + ('group_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='groups.group')), + ], + ), + ] diff --git a/backend/pigeonhole/apps/users/migrations/0001_initial.py b/backend/pigeonhole/apps/users/migrations/0001_initial.py new file mode 100644 index 00000000..4b63fba7 --- /dev/null +++ b/backend/pigeonhole/apps/users/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 5.0.3 on 2024-03-12 13:29 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('courses', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('role', models.IntegerField(choices=[(1, 'Admin'), (2, 'Teacher'), (3, 'Student')], default=3)), + ('course', models.ManyToManyField(to='courses.course')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'db_table': 'auth_user', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] From 518d2874cd79d3860ce388b6906544492c7bef10 Mon Sep 17 00:00:00 2001 From: Reinhard Date: Tue, 12 Mar 2024 15:07:12 +0100 Subject: [PATCH 58/58] courses permissions fixed --- .../pigeonhole/apps/courses/permissions.py | 24 +++++++------------ backend/pigeonhole/apps/courses/views.py | 7 ++++-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/backend/pigeonhole/apps/courses/permissions.py b/backend/pigeonhole/apps/courses/permissions.py index 0361f12a..f7939197 100644 --- a/backend/pigeonhole/apps/courses/permissions.py +++ b/backend/pigeonhole/apps/courses/permissions.py @@ -1,4 +1,5 @@ from rest_framework import permissions + from backend.pigeonhole.apps.users.models import User @@ -6,24 +7,17 @@ class CourseUserPermissions(permissions.BasePermission): def has_permission(self, request, view): if request.user.is_admin or request.user.is_superuser: return True - - if request.user.is_teacher: - return True - - if request.user.is_student or request.user.is_teacher: - return view.action in ['list', 'retrieve'] - - return False - def has_object_permission(self, request, view, obj): - if request.user.is_admin or request.user.is_superuser: - return True if request.user.is_teacher: - if User.objects.filter(id=request.user.id, course=obj).exists(): + if view.action in ['create', 'list', 'retrieve']: return True - return view.action in ['list', 'retrieve'] + elif view.action in ['update', 'partial_update', 'destroy'] and User.objects.filter(id=request.user.id, + course=view.kwargs[ + 'pk']).exists(): + return True + return - if request.user.is_student: + if request.user.is_student or request.user.is_teacher: return view.action in ['list', 'retrieve'] - return False \ No newline at end of file + return False diff --git a/backend/pigeonhole/apps/courses/views.py b/backend/pigeonhole/apps/courses/views.py index 506c6f9b..ff565f65 100644 --- a/backend/pigeonhole/apps/courses/views.py +++ b/backend/pigeonhole/apps/courses/views.py @@ -1,7 +1,7 @@ from rest_framework import viewsets, status +from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.decorators import action from backend.pigeonhole.apps.users.models import User from .models import Course, CourseSerializer @@ -36,6 +36,10 @@ def destroy(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) def list(self, request, *args, **kwargs): + serializer = CourseSerializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def my_list(self, request, *args, **kwargs): if request.user.is_admin or request.user.is_superuser: serializer = CourseSerializer(self.queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -68,4 +72,3 @@ def join_course(self, request, *args, **kwargs): user = User.objects.get(id=request.user.id) user.course.add(course) return Response(status=status.HTTP_200_OK) -