From e23df0cc0437a01f5f65f1b74b1aea4047b7f5a7 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Oude Date: Wed, 22 Feb 2023 17:44:10 +0100 Subject: [PATCH 0001/1000] auto format support + warning on linting error --- .github/workflows/formatting.yml | 88 ++++++++++++++++++++++++++++++++ .gitignore | 4 ++ 2 files changed, 92 insertions(+) create mode 100644 .github/workflows/formatting.yml create mode 100644 .gitignore diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 00000000..bcb4ac31 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,88 @@ +name: Code Formatting and Linting + +on: + # all branches when pushing, only pull request for the main branch + push: + branches: ['**'] + pull_request: + branches: [main] + +jobs: + format_and_lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # using `black` + - name: Format Python code + uses: docker://python:3.8 + with: + args: | + pip install black + black . + + # using `flake8` + - name: Lint Python code + id: python-lint + uses: docker://python:3.8 + with: + args: | + pip install flake8 + flake8 . + + # using `prettier` + - name: Format JavaScript code + uses: docker://node:16 + with: + args: | + npm install --global prettier + prettier --write "**/*.js" + + # using `eslint` + - name: Lint JavaScript code + id: javascript-lint + uses: docker://node:16 + with: + args: | + npm install --global eslint + eslint "**/*.js" + + # using `sqlparse` + - name: Format SQL code + uses: docker://python:3.8 + with: + args: | + pip install sqlparse + find . -name "*.sql" -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \; + + # using `sqlfluff` + - name: Lint SQL code + id: sql-lint + uses: docker://python:3.8 + with: + args: | + pip install sqlfluff + sqlfluff lint + + # if either one has a linting error when executing a pull request, give an error and don't allow the request + - name: Check for linting errors + if: ${{ github.event_name == 'pull_request' }} + run: | + if [ ${{ steps.python-lint.outcome }} == 'failure' ] || [ ${{ steps.javascript-lint.outcome }} == 'failure' ] || [ ${{ steps.sql-lint.outcome }} == 'failure' ]; then + echo "Linting failed. Please fix the errors before merging the pull request." + exit 1 + fi + + # if no linting errors, format code and finalize request + - name: Commit changes + if: ${{ success() }} + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + if git diff --name-only | grep -E '\.py$|\.js$|\.sql$'; then + git add . + git commit -m "Auto-format and lint code" + git push + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..44fefe2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +.vscode + +__pycache__ From aaac2c3a3b3c4b29183f62c6555d121d8701e276 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Oude Date: Wed, 22 Feb 2023 17:53:08 +0100 Subject: [PATCH 0002/1000] test python auto-formatting --- formatting_test_file.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 formatting_test_file.py diff --git a/formatting_test_file.py b/formatting_test_file.py new file mode 100644 index 00000000..8ee0c266 --- /dev/null +++ b/formatting_test_file.py @@ -0,0 +1,25 @@ +j = [1, + 2, + 3 +] + +if 1 == 1 \ + and 2 == 2: + pass + +def foo(): + + print("All the newlines above me should be deleted!") + + +if True: + + print("No newline above me!") + + print("There is a newline above me, and that's OK!") + + +class Point: + + x: int + y: int \ No newline at end of file From afa143d6bc0205634a2a28e88be04c11380077bd Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 20:41:20 +0100 Subject: [PATCH 0003/1000] possible fix formatting python code --- .github/workflows/formatting.yml | 173 +++++++++++++++---------------- 1 file changed, 86 insertions(+), 87 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index bcb4ac31..4e41311c 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,88 +1,87 @@ -name: Code Formatting and Linting - -on: - # all branches when pushing, only pull request for the main branch - push: - branches: ['**'] - pull_request: - branches: [main] - -jobs: - format_and_lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - # using `black` - - name: Format Python code - uses: docker://python:3.8 - with: - args: | - pip install black - black . - - # using `flake8` - - name: Lint Python code - id: python-lint - uses: docker://python:3.8 - with: - args: | - pip install flake8 - flake8 . - - # using `prettier` - - name: Format JavaScript code - uses: docker://node:16 - with: - args: | - npm install --global prettier - prettier --write "**/*.js" - - # using `eslint` - - name: Lint JavaScript code - id: javascript-lint - uses: docker://node:16 - with: - args: | - npm install --global eslint - eslint "**/*.js" - - # using `sqlparse` - - name: Format SQL code - uses: docker://python:3.8 - with: - args: | - pip install sqlparse - find . -name "*.sql" -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \; - - # using `sqlfluff` - - name: Lint SQL code - id: sql-lint - uses: docker://python:3.8 - with: - args: | - pip install sqlfluff - sqlfluff lint - - # if either one has a linting error when executing a pull request, give an error and don't allow the request - - name: Check for linting errors - if: ${{ github.event_name == 'pull_request' }} - run: | - if [ ${{ steps.python-lint.outcome }} == 'failure' ] || [ ${{ steps.javascript-lint.outcome }} == 'failure' ] || [ ${{ steps.sql-lint.outcome }} == 'failure' ]; then - echo "Linting failed. Please fix the errors before merging the pull request." - exit 1 - fi - - # if no linting errors, format code and finalize request - - name: Commit changes - if: ${{ success() }} - run: | - git config --global user.email "group4@selab2.com" - git config --global user.name "Group 4" - if git diff --name-only | grep -E '\.py$|\.js$|\.sql$'; then - git add . - git commit -m "Auto-format and lint code" - git push +name: Code Formatting and Linting + +on: + # all branches when pushing, only pull request for the main branch + push: + branches: ['**'] + pull_request: + branches: [main] + +jobs: + format_and_lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # using `black` + - name: Format Python code + uses: docker://python:3.8 + with: + args: | + sh -c "pip install black && black ." + + # using `flake8` + - name: Lint Python code + id: python-lint + uses: docker://python:3.8 + with: + args: | + pip install flake8 + flake8 . + + # using `prettier` + - name: Format JavaScript code + uses: docker://node:16 + with: + args: | + npm install --global prettier + prettier --write "**/*.js" + + # using `eslint` + - name: Lint JavaScript code + id: javascript-lint + uses: docker://node:16 + with: + args: | + npm install --global eslint + eslint "**/*.js" + + # using `sqlparse` + - name: Format SQL code + uses: docker://python:3.8 + with: + args: | + pip install sqlparse + find . -name "*.sql" -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \; + + # using `sqlfluff` + - name: Lint SQL code + id: sql-lint + uses: docker://python:3.8 + with: + args: | + pip install sqlfluff + sqlfluff lint + + # if either one has a linting error when executing a pull request, give an error and don't allow the request + - name: Check for linting errors + if: ${{ github.event_name == 'pull_request' }} + run: | + if [ ${{ steps.python-lint.outcome }} == 'failure' ] || [ ${{ steps.javascript-lint.outcome }} == 'failure' ] || [ ${{ steps.sql-lint.outcome }} == 'failure' ]; then + echo "Linting failed. Please fix the errors before merging the pull request." + exit 1 + fi + + # if no linting errors, format code and finalize request + - name: Commit changes + if: ${{ success() }} + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + if git diff --name-only | grep -E '\.py$|\.js$|\.sql$'; then + git add . + git commit -m "Auto-format and lint code" + git push fi \ No newline at end of file From e9fd9ff0d8bb65d4f334979e8e1c8a5756c794f0 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 20:41:59 +0100 Subject: [PATCH 0004/1000] new test auto formatting python --- formatting_test_file.py | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/formatting_test_file.py b/formatting_test_file.py index 8ee0c266..56969aed 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,25 +1,25 @@ -j = [1, - 2, - 3 -] - -if 1 == 1 \ - and 2 == 2: - pass - -def foo(): - - print("All the newlines above me should be deleted!") - - -if True: - - print("No newline above me!") - - print("There is a newline above me, and that's OK!") - - -class Point: - - x: int +j = [1, + 2, + 3 +] + +if 1 == 1 \ + and 2 == 2: + pass + +def foo(): + + print("All the newlines above me should be deleted!") + + +if True: + + print("No newline above me!") + + print("There is a newline above me, and that's OK!") + + +class Point: + + x: int y: int \ No newline at end of file From b7fc8290e435081fba0f18bf385fb6d636e02460 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 20:46:51 +0100 Subject: [PATCH 0005/1000] possible fix for other formatting and linting --- .github/workflows/formatting.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 4e41311c..9d8a9511 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -28,16 +28,14 @@ jobs: uses: docker://python:3.8 with: args: | - pip install flake8 - flake8 . + sh -c "pip install flake8 && flake8 ." # using `prettier` - name: Format JavaScript code uses: docker://node:16 with: args: | - npm install --global prettier - prettier --write "**/*.js" + sh -c "npm install --global prettier && prettier --write '**/*.js'" # using `eslint` - name: Lint JavaScript code @@ -45,16 +43,14 @@ jobs: uses: docker://node:16 with: args: | - npm install --global eslint - eslint "**/*.js" + sh -c "npm install --global eslint && eslint '**/*.js'" # using `sqlparse` - name: Format SQL code uses: docker://python:3.8 with: args: | - pip install sqlparse - find . -name "*.sql" -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \; + sh -c "pip install sqlparse && find . -name '*.sql' -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \;" # using `sqlfluff` - name: Lint SQL code @@ -62,8 +58,7 @@ jobs: uses: docker://python:3.8 with: args: | - pip install sqlfluff - sqlfluff lint + sh -c "pip install sqlfluff && sqlfluff lint" # if either one has a linting error when executing a pull request, give an error and don't allow the request - name: Check for linting errors From f62fe97a372f8066824a442402d90b8264278d6f Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 20:47:32 +0100 Subject: [PATCH 0006/1000] yet another auto formatting test --- formatting_test_file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/formatting_test_file.py b/formatting_test_file.py index 56969aed..57ac2766 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -9,6 +9,7 @@ def foo(): + print("All the newlines above me should be deleted!") From ae4c96c3cc8ca0191cfd3bee63bab28f882eead2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 20:57:38 +0100 Subject: [PATCH 0007/1000] added checks to format/lint only if that type of file is pushed --- .github/workflows/formatting.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 9d8a9511..28146f48 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -17,6 +17,7 @@ jobs: # using `black` - name: Format Python code + if: `find . -name '*.py' -type f | grep -q .` uses: docker://python:3.8 with: args: | @@ -24,6 +25,7 @@ jobs: # using `flake8` - name: Lint Python code + if: `find . -name '*.py' -type f | grep -q .` id: python-lint uses: docker://python:3.8 with: @@ -32,6 +34,7 @@ jobs: # using `prettier` - name: Format JavaScript code + if: `find . -name '*.js' -type f | grep -q .` uses: docker://node:16 with: args: | @@ -39,6 +42,7 @@ jobs: # using `eslint` - name: Lint JavaScript code + if: `find . -name '*.js' -type f | grep -q .` id: javascript-lint uses: docker://node:16 with: @@ -47,6 +51,7 @@ jobs: # using `sqlparse` - name: Format SQL code + if: `find . -name '*.sql' -type f | grep -q .` uses: docker://python:3.8 with: args: | @@ -54,6 +59,7 @@ jobs: # using `sqlfluff` - name: Lint SQL code + if: `find . -name '*.sql' -type f | grep -q .` id: sql-lint uses: docker://python:3.8 with: From 4cf17d7b5f1580ec7bfab47f3c8a62bcafae767e Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 20:58:41 +0100 Subject: [PATCH 0008/1000] test the new checks --- formatting_test_file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/formatting_test_file.py b/formatting_test_file.py index 57ac2766..e413e0e3 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -10,6 +10,7 @@ def foo(): + print("All the newlines above me should be deleted!") From 39be720610199cffbdfde5de7906c87bb13238fa Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 21:03:14 +0100 Subject: [PATCH 0009/1000] fix syntax error --- .github/workflows/formatting.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 28146f48..60feb65b 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -17,7 +17,7 @@ jobs: # using `black` - name: Format Python code - if: `find . -name '*.py' -type f | grep -q .` + if: "find . -name '*.py' -type f | grep -q ." uses: docker://python:3.8 with: args: | @@ -25,7 +25,7 @@ jobs: # using `flake8` - name: Lint Python code - if: `find . -name '*.py' -type f | grep -q .` + if: "find . -name '*.py' -type f | grep -q ." id: python-lint uses: docker://python:3.8 with: @@ -34,7 +34,7 @@ jobs: # using `prettier` - name: Format JavaScript code - if: `find . -name '*.js' -type f | grep -q .` + if: "find . -name '*.js' -type f | grep -q ." uses: docker://node:16 with: args: | @@ -42,7 +42,7 @@ jobs: # using `eslint` - name: Lint JavaScript code - if: `find . -name '*.js' -type f | grep -q .` + if: "find . -name '*.js' -type f | grep -q ." id: javascript-lint uses: docker://node:16 with: @@ -51,7 +51,7 @@ jobs: # using `sqlparse` - name: Format SQL code - if: `find . -name '*.sql' -type f | grep -q .` + if: "find . -name '*.sql' -type f | grep -q ." uses: docker://python:3.8 with: args: | @@ -59,7 +59,7 @@ jobs: # using `sqlfluff` - name: Lint SQL code - if: `find . -name '*.sql' -type f | grep -q .` + if: "find . -name '*.sql' -type f | grep -q ." id: sql-lint uses: docker://python:3.8 with: From 185c3c1e899799bed5f124e84723aff694c3e0c8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 21:53:56 +0100 Subject: [PATCH 0010/1000] made seperate file for each language to be formatted --- .github/workflows/formatting.yml | 88 --------------------- .github/workflows/formatting_javascript.yml | 56 +++++++++++++ .github/workflows/formatting_python.yml | 55 +++++++++++++ .github/workflows/formatting_sql.yml | 56 +++++++++++++ 4 files changed, 167 insertions(+), 88 deletions(-) delete mode 100644 .github/workflows/formatting.yml create mode 100644 .github/workflows/formatting_javascript.yml create mode 100644 .github/workflows/formatting_python.yml create mode 100644 .github/workflows/formatting_sql.yml diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml deleted file mode 100644 index 60feb65b..00000000 --- a/.github/workflows/formatting.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Code Formatting and Linting - -on: - # all branches when pushing, only pull request for the main branch - push: - branches: ['**'] - pull_request: - branches: [main] - -jobs: - format_and_lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - # using `black` - - name: Format Python code - if: "find . -name '*.py' -type f | grep -q ." - uses: docker://python:3.8 - with: - args: | - sh -c "pip install black && black ." - - # using `flake8` - - name: Lint Python code - if: "find . -name '*.py' -type f | grep -q ." - id: python-lint - uses: docker://python:3.8 - with: - args: | - sh -c "pip install flake8 && flake8 ." - - # using `prettier` - - name: Format JavaScript code - if: "find . -name '*.js' -type f | grep -q ." - uses: docker://node:16 - with: - args: | - sh -c "npm install --global prettier && prettier --write '**/*.js'" - - # using `eslint` - - name: Lint JavaScript code - if: "find . -name '*.js' -type f | grep -q ." - id: javascript-lint - uses: docker://node:16 - with: - args: | - sh -c "npm install --global eslint && eslint '**/*.js'" - - # using `sqlparse` - - name: Format SQL code - if: "find . -name '*.sql' -type f | grep -q ." - uses: docker://python:3.8 - with: - args: | - sh -c "pip install sqlparse && find . -name '*.sql' -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \;" - - # using `sqlfluff` - - name: Lint SQL code - if: "find . -name '*.sql' -type f | grep -q ." - id: sql-lint - uses: docker://python:3.8 - with: - args: | - sh -c "pip install sqlfluff && sqlfluff lint" - - # if either one has a linting error when executing a pull request, give an error and don't allow the request - - name: Check for linting errors - if: ${{ github.event_name == 'pull_request' }} - run: | - if [ ${{ steps.python-lint.outcome }} == 'failure' ] || [ ${{ steps.javascript-lint.outcome }} == 'failure' ] || [ ${{ steps.sql-lint.outcome }} == 'failure' ]; then - echo "Linting failed. Please fix the errors before merging the pull request." - exit 1 - fi - - # if no linting errors, format code and finalize request - - name: Commit changes - if: ${{ success() }} - run: | - git config --global user.email "group4@selab2.com" - git config --global user.name "Group 4" - if git diff --name-only | grep -E '\.py$|\.js$|\.sql$'; then - git add . - git commit -m "Auto-format and lint code" - git push - fi \ No newline at end of file diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml new file mode 100644 index 00000000..ed5d708c --- /dev/null +++ b/.github/workflows/formatting_javascript.yml @@ -0,0 +1,56 @@ +formatting_python: + yml: +name: Code Formatting and Linting + +on: + # all branches when pushing, only pull request for the main branch + push: + branches: ['**'] + paths: + - '**.js' + pull_request: + branches: [main] + +jobs: + format_and_lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # using `prettier` + - name: Format JavaScript code + uses: docker://node:16 + with: + args: | + sh -c "npm install --global prettier && prettier --write '**/*.js'" + + # using `eslint` + - name: Lint JavaScript code + id: javascript-lint + uses: docker://node:16 + with: + args: | + sh -c "npm install --global eslint && eslint '**/*.js'" + + # if either one has a linting error when executing a pull request, give an error and don't allow the request + - name: Check for linting errors + if: ${{ github.event_name == 'pull_request' }} + run: | + if [ ${{ steps.javascript-lint.outcome }} == 'failure' ]; then + echo "Linting of JavaScript files failed. Please fix the errors before merging the pull request." + exit 1 + fi + + # if no linting errors, format code and finalize request + - name: Commit changes + if: ${{ success() }} + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + if git diff --name-only | grep -E '\.js$'; then + git add . + git commit -m "Auto-format and lint JavaScript code" + git push + fi \ No newline at end of file diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml new file mode 100644 index 00000000..7d90290d --- /dev/null +++ b/.github/workflows/formatting_python.yml @@ -0,0 +1,55 @@ +name: Code Formatting and Linting + +on: + # all branches when pushing, only pull request for the main branch + push: + branches: ['**'] + paths: + - '**.py' + pull_request: + branches: [main] + +jobs: + format_and_lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # using `black` + - name: Format Python code + uses: docker://python:3.8 + with: + args: | + sh -c "pip install black && black ." + + # using `flake8` + - name: Lint Python code + id: python-lint + uses: docker://python:3.8 + with: + args: | + sh -c "pip install flake8 && flake8 ." + + + # if there is a linting error when executing a pull request, give an error and don't allow the request + - name: Check for linting errors + if: ${{ github.event_name == 'pull_request' }} + run: | + if [ ${{ steps.python-lint.outcome }} == 'failure' ]; then + echo "Linting of Python files failed. Please fix the errors before merging the pull request." + exit 1 + fi + + # if no linting errors, format code and finalize request + - name: Commit changes + if: ${{ success() }} + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + if git diff --name-only | grep -E '\.py$'; then + git add . + git commit -m "Auto-format and lint Python code" + git push + fi \ No newline at end of file diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml new file mode 100644 index 00000000..6368aba9 --- /dev/null +++ b/.github/workflows/formatting_sql.yml @@ -0,0 +1,56 @@ +formatting_python: + yml: +name: Code Formatting and Linting + +on: + # all branches when pushing, only pull request for the main branch + push: + branches: ['**'] + paths: + - '**.sql' + pull_request: + branches: [main] + +jobs: + format_and_lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # using `sqlparse` + - name: Format SQL code + uses: docker://python:3.8 + with: + args: | + sh -c "pip install sqlparse && find . -name '*.sql' -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \;" + + # using `sqlfluff` + - name: Lint SQL code + id: sql-lint + uses: docker://python:3.8 + with: + args: | + sh -c "pip install sqlfluff && sqlfluff lint" + + # if either one has a linting error when executing a pull request, give an error and don't allow the request + - name: Check for linting errors + if: ${{ github.event_name == 'pull_request' }} + run: | + if [ ${{ steps.sql-lint.outcome }} == 'failure' ]; then + echo "Linting of SQL files failed. Please fix the errors before merging the pull request." + exit 1 + fi + + # if no linting errors, format code and finalize request + - name: Commit changes + if: ${{ success() }} + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + if git diff --name-only | grep -E '\.sql$'; then + git add . + git commit -m "Auto-format and lint SQL code" + git push + fi \ No newline at end of file From 9821ef859c4dce35b78e1af11c5e947f30e621a9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 21:55:32 +0100 Subject: [PATCH 0011/1000] test seperate python formatting --- formatting_test_file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/formatting_test_file.py b/formatting_test_file.py index e413e0e3..34a3c8bb 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -11,6 +11,7 @@ def foo(): + print("All the newlines above me should be deleted!") From f5df28753a81e668c2b44ec11b58fe2442fd034d Mon Sep 17 00:00:00 2001 From: Group 4 Date: Wed, 22 Feb 2023 20:55:58 +0000 Subject: [PATCH 0012/1000] Auto-format and lint Python code --- formatting_test_file.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/formatting_test_file.py b/formatting_test_file.py index 34a3c8bb..97dc162d 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,28 +1,19 @@ -j = [1, - 2, - 3 -] +j = [1, 2, 3] -if 1 == 1 \ - and 2 == 2: +if 1 == 1 and 2 == 2: pass -def foo(): - - - +def foo(): print("All the newlines above me should be deleted!") if True: - print("No newline above me!") print("There is a newline above me, and that's OK!") class Point: - x: int - y: int \ No newline at end of file + y: int From 052c8057feeef8b027112ba515772293487b8dfa Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 22:29:07 +0100 Subject: [PATCH 0013/1000] added test files for javascript and sql --- formatting_test_file.js | 21 +++++++++++++++++++++ formatting_test_file.sql | 17 +++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 formatting_test_file.js create mode 100644 formatting_test_file.sql diff --git a/formatting_test_file.js b/formatting_test_file.js new file mode 100644 index 00000000..3e0adbac --- /dev/null +++ b/formatting_test_file.js @@ -0,0 +1,21 @@ +function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, onMouseOver,}) { + + if(!greeting){return null}; + + // TODO: Don't use random in render + let num = Math.floor (Math.random() * 1E+7).toString().replace(/\.\d+/ig, "") + + return
+ + { greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() } + {greeting.endsWith(",") ? " " : ", " } + + { greeted } + + { (silent) + ? "." + : "!"} + +
; + +} \ No newline at end of file diff --git a/formatting_test_file.sql b/formatting_test_file.sql new file mode 100644 index 00000000..f370e8f2 --- /dev/null +++ b/formatting_test_file.sql @@ -0,0 +1,17 @@ +CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); +INSERT INTO + MortgageCompanies +VALUES + (1, 'Quicken Loans'); +INSERT INTO + MortgageCompanies +VALUES + (2, 'Wells Fargo Bank'); +INSERT INTO + MortgageCompanies +VALUES + (3, 'JPMorgan Chase Bank'); +SELECT + * +FROM + MortgageCompanies; \ No newline at end of file From c9d1b9a29d7761237951d398bf6ed58094224bd0 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 22:36:10 +0100 Subject: [PATCH 0014/1000] fixed small syntax errors --- .github/workflows/formatting_javascript.yml | 2 +- .github/workflows/formatting_sql.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml index ed5d708c..9d617cfd 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/formatting_javascript.yml @@ -1,4 +1,4 @@ -formatting_python: +formatting_javascript: yml: name: Code Formatting and Linting diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml index 6368aba9..57bec6e6 100644 --- a/.github/workflows/formatting_sql.yml +++ b/.github/workflows/formatting_sql.yml @@ -1,4 +1,4 @@ -formatting_python: +formatting_sql: yml: name: Code Formatting and Linting From baccdb386ecff9dbb0c944d2efdf91c0c59ba727 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 22:39:14 +0100 Subject: [PATCH 0015/1000] retry tests auto formatting sql and js --- formatting_test_file.js | 1 + formatting_test_file.sql | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 3e0adbac..4f9cabcd 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -2,6 +2,7 @@ function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, on if(!greeting){return null}; + // TODO: Don't use random in render let num = Math.floor (Math.random() * 1E+7).toString().replace(/\.\d+/ig, "") diff --git a/formatting_test_file.sql b/formatting_test_file.sql index f370e8f2..2d0e3c98 100644 --- a/formatting_test_file.sql +++ b/formatting_test_file.sql @@ -1,5 +1,5 @@ CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); -INSERT INTO +INSERT INTO MortgageCompanies VALUES (1, 'Quicken Loans'); From 7f5a1e83bf3b58af0e33eae0e463c09e51f25efe Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 22:44:10 +0100 Subject: [PATCH 0016/1000] possible fix same error in all yml files --- .github/workflows/formatting_javascript.yml | 4 +--- .github/workflows/formatting_python.yml | 2 +- .github/workflows/formatting_sql.yml | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml index 9d617cfd..c94370f2 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/formatting_javascript.yml @@ -1,6 +1,4 @@ -formatting_javascript: - yml: -name: Code Formatting and Linting +name: Code Formatting and Linting JavaScript on: # all branches when pushing, only pull request for the main branch diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml index 7d90290d..8717132b 100644 --- a/.github/workflows/formatting_python.yml +++ b/.github/workflows/formatting_python.yml @@ -1,4 +1,4 @@ -name: Code Formatting and Linting +name: Code Formatting and Linting Python on: # all branches when pushing, only pull request for the main branch diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml index 57bec6e6..f1605b49 100644 --- a/.github/workflows/formatting_sql.yml +++ b/.github/workflows/formatting_sql.yml @@ -1,6 +1,4 @@ -formatting_sql: - yml: -name: Code Formatting and Linting +name: Code Formatting and Linting SQL on: # all branches when pushing, only pull request for the main branch From 91d4a87687b1b0e63becb5dbb836a4679ac48a55 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Wed, 22 Feb 2023 22:48:10 +0100 Subject: [PATCH 0017/1000] test if all three formatters work together --- formatting_test_file.js | 1 + formatting_test_file.py | 19 +++++++++++++++++-- formatting_test_file.sql | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 4f9cabcd..4d581528 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -1,5 +1,6 @@ function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, onMouseOver,}) { + if(!greeting){return null}; diff --git a/formatting_test_file.py b/formatting_test_file.py index 97dc162d..6a1064f5 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,19 +1,34 @@ -j = [1, 2, 3] +j = [1, +2, +3 +] -if 1 == 1 and 2 == 2: +if 1 == 1 \ + and 2 == 2: pass def foo(): + + + + + print("All the newlines above me should be deleted!") if True: print("No newline above me!") + + + print("There is a newline above me, and that's OK!") class Point: + + x: int + y: int diff --git a/formatting_test_file.sql b/formatting_test_file.sql index 2d0e3c98..f370e8f2 100644 --- a/formatting_test_file.sql +++ b/formatting_test_file.sql @@ -1,5 +1,5 @@ CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); -INSERT INTO +INSERT INTO MortgageCompanies VALUES (1, 'Quicken Loans'); From a7ca1d482b5b9026546a6b139968690c995e2862 Mon Sep 17 00:00:00 2001 From: Group 4 Date: Wed, 22 Feb 2023 21:48:43 +0000 Subject: [PATCH 0018/1000] Auto-format and lint Python code --- formatting_test_file.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/formatting_test_file.py b/formatting_test_file.py index 6a1064f5..2739d124 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,34 +1,20 @@ -j = [1, -2, -3 -] +j = [1, 2, 3] -if 1 == 1 \ - and 2 == 2: +if 1 == 1 and 2 == 2: pass def foo(): - - - - - print("All the newlines above me should be deleted!") if True: print("No newline above me!") - - - print("There is a newline above me, and that's OK!") class Point: - - x: int y: int From 0fc50878a57cb454335904e0544a4f720e42794b Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 14:40:21 +0100 Subject: [PATCH 0019/1000] added config file for javascript linting --- .eslintrc.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..d937af9d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "env": { + "browser": true, + "node": true, + "es6": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "plugins": [ + "react", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "no-console": "off", + "no-unused-vars": "warn" + } +} \ No newline at end of file From 150f6771eee6537009cfb9d6865084e40e81e7b6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 14:55:57 +0100 Subject: [PATCH 0020/1000] new javascript linting test --- formatting_test_file.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/formatting_test_file.js b/formatting_test_file.js index 4d581528..d169c0fe 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -9,7 +9,9 @@ function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, on return
+ { greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() } + {greeting.endsWith(",") ? " " : ", " } { greeted } From a9d0d38a3fe32ce1f7fa5910821f9959b6a2a491 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 14:59:02 +0100 Subject: [PATCH 0021/1000] don't use react plugin yet --- .eslintrc.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d937af9d..04b0a695 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,8 +5,8 @@ "es6": true }, "extends": [ - "eslint:recommended", - "plugin:react/recommended" + "eslint:recommended" + // "plugin:react/recommended" ], "parserOptions": { "ecmaVersion": 2022, From d16807806bda29c7a2d9d32caf2e27b94dcf9574 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:00:01 +0100 Subject: [PATCH 0022/1000] test javascript linting, no react plugin --- formatting_test_file.js | 1 + 1 file changed, 1 insertion(+) diff --git a/formatting_test_file.js b/formatting_test_file.js index d169c0fe..c8ee5a76 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -13,6 +13,7 @@ function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, on { greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() } {greeting.endsWith(",") ? " " : ", " } + { greeted } From 40d02b49059e345e84efa951c0027600efdd1fbe Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:01:52 +0100 Subject: [PATCH 0023/1000] fix syntax error --- .eslintrc.json | 2 +- formatting_test_file.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 04b0a695..8895d81b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,7 @@ "sourceType": "module" }, "plugins": [ - "react", + // "react", "prettier" ], "rules": { diff --git a/formatting_test_file.js b/formatting_test_file.js index c8ee5a76..6715b554 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -11,9 +11,8 @@ function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, on { greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() } - {greeting.endsWith(",") ? " " : ", " } - + { greeted } From d37ec79385f952fc2f123d07e776a0f9aaf441e4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:03:50 +0100 Subject: [PATCH 0024/1000] not using any plugins yet whatsoever, will do so after project setup --- .eslintrc.json | 8 ++++---- formatting_test_file.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 8895d81b..30887016 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,12 +12,12 @@ "ecmaVersion": 2022, "sourceType": "module" }, - "plugins": [ + //"plugins": [ // "react", - "prettier" - ], + //"prettier" + //], "rules": { - "prettier/prettier": "error", + //"prettier/prettier": "error", "no-console": "off", "no-unused-vars": "warn" } diff --git a/formatting_test_file.js b/formatting_test_file.js index 6715b554..3f9b98d6 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -11,6 +11,7 @@ function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, on { greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() } + {greeting.endsWith(",") ? " " : ", " } From 5373d6c6312a5cd6d0ba4ba3f7c4921f234be6f3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:10:15 +0100 Subject: [PATCH 0025/1000] new (simpler) test code --- formatting_test_file.js | 45 +++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 3f9b98d6..35167303 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -1,26 +1,41 @@ -function HelloWorld({greeting = "hello", greeted = '"World"', silent = false, onMouseOver,}) { +// program to find the factorial of a number - - if(!greeting){return null}; +// take input from the user - // TODO: Don't use random in render - let num = Math.floor (Math.random() * 1E+7).toString().replace(/\.\d+/ig, "") - return
+const number = parseInt( + prompt('Enter a positive integer: ')); - { greeting.slice( 0, 1 ).toUpperCase() + greeting.slice(1).toLowerCase() } +// checking if number is negative +if (number < 0) { - {greeting.endsWith(",") ? " " : ", " } - - { greeted } - - { (silent) - ? "." - : "!"} + console.log('Error! ' + + 'Factorial for negative ' + + 'number does not exist.' -
; + ); + +} + +// if number is 0 +else if (number === 0) { + + + console.log(`The factorial of ${number} is 1.`); +} + +// if number is positive +else { + + + let fact = 1; + for (i = 1; i <= number; i++) { + fact *= i; + } + + console.log(`The factorial of ${number} is ${fact}.`); } \ No newline at end of file From 55cdd4154c72ef12af01b22cee8a57777a0f7435 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:13:16 +0100 Subject: [PATCH 0026/1000] rewritten test code to actually work --- formatting_test_file.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 35167303..66dd44ef 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -3,10 +3,7 @@ // take input from the user - -const number = parseInt( - - prompt('Enter a positive integer: ')); +const number = 5; // checking if number is negative if (number < 0) { @@ -33,9 +30,9 @@ else { let fact = 1; - for (i = 1; i <= number; i++) { + for (let i = 1; i <= number; i++) { fact *= i; } - + console.log(`The factorial of ${number} is ${fact}.`); } \ No newline at end of file From 51ba37972ccfd9c10c63790b2509e2e858f193be Mon Sep 17 00:00:00 2001 From: Group 4 Date: Thu, 23 Feb 2023 14:13:53 +0000 Subject: [PATCH 0027/1000] Auto-format and lint JavaScript code --- formatting_test_file.js | 63 ++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 66dd44ef..27379eb0 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -1,38 +1,25 @@ -// program to find the factorial of a number - -// take input from the user - - -const number = 5; - -// checking if number is negative -if (number < 0) { - - - console.log('Error! ' + - 'Factorial for negative ' + - 'number does not exist.' - - ); - - -} - -// if number is 0 -else if (number === 0) { - - - console.log(`The factorial of ${number} is 1.`); -} - -// if number is positive -else { - - - let fact = 1; - for (let i = 1; i <= number; i++) { - fact *= i; - } - - console.log(`The factorial of ${number} is ${fact}.`); -} \ No newline at end of file +// program to find the factorial of a number + +// take input from the user + +const number = 5; + +// checking if number is negative +if (number < 0) { + console.log("Error! " + "Factorial for negative " + "number does not exist."); +} + +// if number is 0 +else if (number === 0) { + console.log(`The factorial of ${number} is 1.`); +} + +// if number is positive +else { + let fact = 1; + for (let i = 1; i <= number; i++) { + fact *= i; + } + + console.log(`The factorial of ${number} is ${fact}.`); +} From 5e3166a87b9affe3291abc7678118f2cd58af3d4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:28:48 +0100 Subject: [PATCH 0028/1000] specified the postgres dialect when formatting sql --- .github/workflows/formatting_sql.yml | 2 +- formatting_test_file.sql | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml index f1605b49..565c7b91 100644 --- a/.github/workflows/formatting_sql.yml +++ b/.github/workflows/formatting_sql.yml @@ -30,7 +30,7 @@ jobs: uses: docker://python:3.8 with: args: | - sh -c "pip install sqlfluff && sqlfluff lint" + sh -c "pip install sqlfluff && sqlfluff lint --dialect postgres" # if either one has a linting error when executing a pull request, give an error and don't allow the request - name: Check for linting errors diff --git a/formatting_test_file.sql b/formatting_test_file.sql index f370e8f2..5a27400d 100644 --- a/formatting_test_file.sql +++ b/formatting_test_file.sql @@ -1,4 +1,5 @@ CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); + INSERT INTO MortgageCompanies VALUES From bbc146bb165f2286a7902716092e99af9076c6d7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:34:56 +0100 Subject: [PATCH 0029/1000] fix syntax error sql tests --- formatting_test_file.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/formatting_test_file.sql b/formatting_test_file.sql index 5a27400d..f370e8f2 100644 --- a/formatting_test_file.sql +++ b/formatting_test_file.sql @@ -1,5 +1,4 @@ CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); - INSERT INTO MortgageCompanies VALUES From 1d89b1add35e71767ada959706ff865fd30064e9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:45:35 +0100 Subject: [PATCH 0030/1000] prevent crashing even before checks --- .github/workflows/formatting_sql.yml | 2 +- formatting_test_file.sql | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml index 565c7b91..47cf293d 100644 --- a/.github/workflows/formatting_sql.yml +++ b/.github/workflows/formatting_sql.yml @@ -30,7 +30,7 @@ jobs: uses: docker://python:3.8 with: args: | - sh -c "pip install sqlfluff && sqlfluff lint --dialect postgres" + sh -c "pip install sqlfluff && sqlfluff lint --dialect postgres --quiet" # if either one has a linting error when executing a pull request, give an error and don't allow the request - name: Check for linting errors diff --git a/formatting_test_file.sql b/formatting_test_file.sql index f370e8f2..67fb9286 100644 --- a/formatting_test_file.sql +++ b/formatting_test_file.sql @@ -1,6 +1,5 @@ CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); -INSERT INTO - MortgageCompanies +INSERT INTO MortgageCompanies VALUES (1, 'Quicken Loans'); INSERT INTO From 173acc4562fb4c403f0891e0d474ede2ef04d754 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 15:51:39 +0100 Subject: [PATCH 0031/1000] specified nofail instead of quiet --- .github/workflows/formatting_sql.yml | 2 +- formatting_test_file.sql | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml index 47cf293d..bf685429 100644 --- a/.github/workflows/formatting_sql.yml +++ b/.github/workflows/formatting_sql.yml @@ -30,7 +30,7 @@ jobs: uses: docker://python:3.8 with: args: | - sh -c "pip install sqlfluff && sqlfluff lint --dialect postgres --quiet" + sh -c "pip install sqlfluff && sqlfluff lint --dialect postgres --nofail" # if either one has a linting error when executing a pull request, give an error and don't allow the request - name: Check for linting errors diff --git a/formatting_test_file.sql b/formatting_test_file.sql index 67fb9286..8a630370 100644 --- a/formatting_test_file.sql +++ b/formatting_test_file.sql @@ -1,5 +1,6 @@ CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); -INSERT INTO MortgageCompanies +INSERT INTO + MortgageCompanies VALUES (1, 'Quicken Loans'); INSERT INTO From 2ee2950dc196a3bcd025fbd2f4c7973c9140489b Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 18:38:14 +0100 Subject: [PATCH 0032/1000] changed to be self hosted on our server --- .github/workflows/formatting_javascript.yml | 2 +- .github/workflows/formatting_python.yml | 2 +- .github/workflows/formatting_sql.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml index c94370f2..bf304ee8 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/formatting_javascript.yml @@ -11,7 +11,7 @@ on: jobs: format_and_lint: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Checkout code diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml index 8717132b..45908fe9 100644 --- a/.github/workflows/formatting_python.yml +++ b/.github/workflows/formatting_python.yml @@ -11,7 +11,7 @@ on: jobs: format_and_lint: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Checkout code diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml index bf685429..dff3485f 100644 --- a/.github/workflows/formatting_sql.yml +++ b/.github/workflows/formatting_sql.yml @@ -11,7 +11,7 @@ on: jobs: format_and_lint: - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Checkout code From 82b5219412e1409c07ada7e25198990f800996f3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 23 Feb 2023 18:39:40 +0100 Subject: [PATCH 0033/1000] test whether self hosting works --- formatting_test_file.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/formatting_test_file.py b/formatting_test_file.py index 2739d124..4586d9b2 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,16 +1,26 @@ -j = [1, 2, 3] +j = [1, + 2, + 3] + +if 1 == 1 \ + and 2 == 2: -if 1 == 1 and 2 == 2: pass def foo(): + print("All the newlines above me should be deleted!") if True: + + + + print("No newline above me!") + print("There is a newline above me, and that's OK!") From 294a9931914350418f73c69e975f82b61124c2cb Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 15:54:19 +0100 Subject: [PATCH 0034/1000] removed formatting and linting support for sql due to performance reasons --- .github/workflows/formatting_sql.yml | 54 ---------------------------- formatting_test_file.sql | 17 --------- 2 files changed, 71 deletions(-) delete mode 100644 .github/workflows/formatting_sql.yml delete mode 100644 formatting_test_file.sql diff --git a/.github/workflows/formatting_sql.yml b/.github/workflows/formatting_sql.yml deleted file mode 100644 index dff3485f..00000000 --- a/.github/workflows/formatting_sql.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Code Formatting and Linting SQL - -on: - # all branches when pushing, only pull request for the main branch - push: - branches: ['**'] - paths: - - '**.sql' - pull_request: - branches: [main] - -jobs: - format_and_lint: - runs-on: self-hosted - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - # using `sqlparse` - - name: Format SQL code - uses: docker://python:3.8 - with: - args: | - sh -c "pip install sqlparse && find . -name '*.sql' -type f -exec sh -c 'sql-formatter -f {} > {}.new && mv {}.new {}' \;" - - # using `sqlfluff` - - name: Lint SQL code - id: sql-lint - uses: docker://python:3.8 - with: - args: | - sh -c "pip install sqlfluff && sqlfluff lint --dialect postgres --nofail" - - # if either one has a linting error when executing a pull request, give an error and don't allow the request - - name: Check for linting errors - if: ${{ github.event_name == 'pull_request' }} - run: | - if [ ${{ steps.sql-lint.outcome }} == 'failure' ]; then - echo "Linting of SQL files failed. Please fix the errors before merging the pull request." - exit 1 - fi - - # if no linting errors, format code and finalize request - - name: Commit changes - if: ${{ success() }} - run: | - git config --global user.email "group4@selab2.com" - git config --global user.name "Group 4" - if git diff --name-only | grep -E '\.sql$'; then - git add . - git commit -m "Auto-format and lint SQL code" - git push - fi \ No newline at end of file diff --git a/formatting_test_file.sql b/formatting_test_file.sql deleted file mode 100644 index 8a630370..00000000 --- a/formatting_test_file.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE MortgageCompanies (ID INTEGER PRIMARY KEY, NAME CHAR(30)); -INSERT INTO - MortgageCompanies -VALUES - (1, 'Quicken Loans'); -INSERT INTO - MortgageCompanies -VALUES - (2, 'Wells Fargo Bank'); -INSERT INTO - MortgageCompanies -VALUES - (3, 'JPMorgan Chase Bank'); -SELECT - * -FROM - MortgageCompanies; \ No newline at end of file From 1c13aaf3a64122cf799034894fa4775b558b709d Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 16:06:24 +0100 Subject: [PATCH 0035/1000] changed github action policy to only work on pull requests to main and develop --- .github/workflows/formatting_javascript.yml | 9 ++++----- .github/workflows/formatting_python.yml | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml index bf304ee8..1d860539 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/formatting_javascript.yml @@ -1,13 +1,12 @@ name: Code Formatting and Linting JavaScript on: - # all branches when pushing, only pull request for the main branch - push: - branches: ['**'] + pull_request: + branches: + - main + - develop paths: - '**.js' - pull_request: - branches: [main] jobs: format_and_lint: diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml index 45908fe9..37455a23 100644 --- a/.github/workflows/formatting_python.yml +++ b/.github/workflows/formatting_python.yml @@ -1,13 +1,12 @@ name: Code Formatting and Linting Python on: - # all branches when pushing, only pull request for the main branch - push: - branches: ['**'] + pull_request: + branches: + - main + - develop paths: - '**.py' - pull_request: - branches: [main] jobs: format_and_lint: From 860d2c16251142615363db89d4133d350113325e Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 16:08:41 +0100 Subject: [PATCH 0036/1000] changed test files a bit to be able to test the formatting --- formatting_test_file.js | 22 +++++++++++++++++----- formatting_test_file.py | 1 - 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 27379eb0..1a653ee5 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -2,21 +2,33 @@ // take input from the user -const number = 5; +const number + = 5; // checking if number is negative -if (number < 0) { - console.log("Error! " + "Factorial for negative " + "number does not exist."); +if ( + number + < 0) { + console.log("Error! " + + "Factorial for negative " + + "number does not exist." + + ); } // if number is 0 -else if (number === 0) { +else if + + +(number === 0) { console.log(`The factorial of ${number} is 1.`); } // if number is positive else { - let fact = 1; + let fact + + = 1; for (let i = 1; i <= number; i++) { fact *= i; } diff --git a/formatting_test_file.py b/formatting_test_file.py index 4586d9b2..a095d0ad 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -17,7 +17,6 @@ def foo(): - print("No newline above me!") From 0c97988f8d4d08da7b9864b316221891c89a755c Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 16:13:34 +0100 Subject: [PATCH 0037/1000] temporary test that also runs on pushes --- .github/workflows/formatting_javascript.yml | 4 ++++ .github/workflows/formatting_python.yml | 4 ++++ formatting_test_file.js | 3 ++- formatting_test_file.py | 3 ++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml index 1d860539..37603f2e 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/formatting_javascript.yml @@ -1,6 +1,10 @@ name: Code Formatting and Linting JavaScript on: + push: + branches: [ '**' ] + paths: + - '**.js' pull_request: branches: - main diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml index 37455a23..92a02112 100644 --- a/.github/workflows/formatting_python.yml +++ b/.github/workflows/formatting_python.yml @@ -1,6 +1,10 @@ name: Code Formatting and Linting Python on: + push: + branches: [ '**' ] + paths: + - '**.py' pull_request: branches: - main diff --git a/formatting_test_file.js b/formatting_test_file.js index 1a653ee5..793d8880 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -10,7 +10,8 @@ if ( number < 0) { console.log("Error! " + - "Factorial for negative " + + "Factorial" + + " for negative " + "number does not exist." ); diff --git a/formatting_test_file.py b/formatting_test_file.py index a095d0ad..1230a838 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -3,7 +3,8 @@ 3] if 1 == 1 \ - and 2 == 2: + and 2 ==\ + 2: pass From d9402eacdbfd53c2efd7a5331d64dd10a3b1bf80 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 20:29:54 +0100 Subject: [PATCH 0038/1000] testing self hosted runner --- formatting_test_file.js | 1 + formatting_test_file.py | 1 + 2 files changed, 2 insertions(+) diff --git a/formatting_test_file.js b/formatting_test_file.js index 793d8880..94efe519 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -21,6 +21,7 @@ if ( else if + (number === 0) { console.log(`The factorial of ${number} is 1.`); } diff --git a/formatting_test_file.py b/formatting_test_file.py index 1230a838..09f6409e 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -6,6 +6,7 @@ and 2 ==\ 2: + pass From 00c60bbc6e9f841f1c830121f62fbd0365a1c878 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 21:06:43 +0100 Subject: [PATCH 0039/1000] changed auto formatting to stop running on self hosted runners, just to test --- .github/workflows/formatting_javascript.yml | 2 +- .github/workflows/formatting_python.yml | 2 +- formatting_test_file.js | 1 - formatting_test_file.py | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/formatting_javascript.yml index 37603f2e..c56b11d0 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/formatting_javascript.yml @@ -14,7 +14,7 @@ on: jobs: format_and_lint: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml index 92a02112..19d3a65b 100644 --- a/.github/workflows/formatting_python.yml +++ b/.github/workflows/formatting_python.yml @@ -14,7 +14,7 @@ on: jobs: format_and_lint: - runs-on: self-hosted + runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/formatting_test_file.js b/formatting_test_file.js index 94efe519..793d8880 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -21,7 +21,6 @@ if ( else if - (number === 0) { console.log(`The factorial of ${number} is 1.`); } diff --git a/formatting_test_file.py b/formatting_test_file.py index 09f6409e..1230a838 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -6,7 +6,6 @@ and 2 ==\ 2: - pass From bb05a7136a329ffbf758d28954addcf676b18f4c Mon Sep 17 00:00:00 2001 From: Group 4 Date: Tue, 28 Feb 2023 20:08:56 +0000 Subject: [PATCH 0040/1000] Auto-format and lint Python code --- formatting_test_file.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/formatting_test_file.py b/formatting_test_file.py index 1230a838..2739d124 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,26 +1,16 @@ -j = [1, - 2, - 3] - -if 1 == 1 \ - and 2 ==\ - 2: +j = [1, 2, 3] +if 1 == 1 and 2 == 2: pass def foo(): - print("All the newlines above me should be deleted!") if True: - - - print("No newline above me!") - print("There is a newline above me, and that's OK!") From 3cec0de4ee2041e24f7acd20519b456f1f595a9e Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Tue, 28 Feb 2023 21:19:35 +0100 Subject: [PATCH 0041/1000] moved both formatting of python and js to one file --- ...ing_javascript.yml => auto_formatting.yml} | 36 +++++++++--- .github/workflows/formatting_python.yml | 58 ------------------- formatting_test_file.js | 1 + formatting_test_file.py | 1 + 4 files changed, 31 insertions(+), 65 deletions(-) rename .github/workflows/{formatting_javascript.yml => auto_formatting.yml} (51%) delete mode 100644 .github/workflows/formatting_python.yml diff --git a/.github/workflows/formatting_javascript.yml b/.github/workflows/auto_formatting.yml similarity index 51% rename from .github/workflows/formatting_javascript.yml rename to .github/workflows/auto_formatting.yml index c56b11d0..a3114ef0 100644 --- a/.github/workflows/formatting_javascript.yml +++ b/.github/workflows/auto_formatting.yml @@ -1,15 +1,17 @@ -name: Code Formatting and Linting JavaScript +name: Code Formatting and Linting on: push: branches: [ '**' ] paths: + - '**.py' - '**.js' pull_request: branches: - main - develop paths: + - '**.py' - '**.js' jobs: @@ -20,15 +22,35 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - # using `prettier` + # using `black` for Python + - name: Format Python code + if: ${{ github.event_path contains '.py' }} + uses: docker://python:3.8 + with: + args: | + sh -c "pip install black && black ." + + # using `flake8` for Python + - name: Lint Python code + if: ${{ github.event_path contains '.py' }} + id: python-lint + uses: docker://python:3.8 + with: + args: | + sh -c "pip install flake8 && flake8 ." + + + # using `prettier` for JavaScript - name: Format JavaScript code + if: ${{ github.event_path contains '.js' }} uses: docker://node:16 with: args: | sh -c "npm install --global prettier && prettier --write '**/*.js'" - # using `eslint` + # using `eslint` for JavaScript - name: Lint JavaScript code + if: ${{ github.event_path contains '.js' }} id: javascript-lint uses: docker://node:16 with: @@ -39,8 +61,8 @@ jobs: - name: Check for linting errors if: ${{ github.event_name == 'pull_request' }} run: | - if [ ${{ steps.javascript-lint.outcome }} == 'failure' ]; then - echo "Linting of JavaScript files failed. Please fix the errors before merging the pull request." + if [ ${{ steps.python-lint.outcome }} == 'failure' -o ${{ steps.javascript-lint.outcome }} == 'failure' ]; then + echo "Linting of code failed. Please fix the errors before merging the pull request." exit 1 fi @@ -50,8 +72,8 @@ jobs: run: | git config --global user.email "group4@selab2.com" git config --global user.name "Group 4" - if git diff --name-only | grep -E '\.js$'; then + if git diff --name-only | grep -E '\.py$|\.js$'; then git add . - git commit -m "Auto-format and lint JavaScript code" + git commit -m "Auto-format and lint code" git push fi \ No newline at end of file diff --git a/.github/workflows/formatting_python.yml b/.github/workflows/formatting_python.yml deleted file mode 100644 index 19d3a65b..00000000 --- a/.github/workflows/formatting_python.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Code Formatting and Linting Python - -on: - push: - branches: [ '**' ] - paths: - - '**.py' - pull_request: - branches: - - main - - develop - paths: - - '**.py' - -jobs: - format_and_lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - # using `black` - - name: Format Python code - uses: docker://python:3.8 - with: - args: | - sh -c "pip install black && black ." - - # using `flake8` - - name: Lint Python code - id: python-lint - uses: docker://python:3.8 - with: - args: | - sh -c "pip install flake8 && flake8 ." - - - # if there is a linting error when executing a pull request, give an error and don't allow the request - - name: Check for linting errors - if: ${{ github.event_name == 'pull_request' }} - run: | - if [ ${{ steps.python-lint.outcome }} == 'failure' ]; then - echo "Linting of Python files failed. Please fix the errors before merging the pull request." - exit 1 - fi - - # if no linting errors, format code and finalize request - - name: Commit changes - if: ${{ success() }} - run: | - git config --global user.email "group4@selab2.com" - git config --global user.name "Group 4" - if git diff --name-only | grep -E '\.py$'; then - git add . - git commit -m "Auto-format and lint Python code" - git push - fi \ No newline at end of file diff --git a/formatting_test_file.js b/formatting_test_file.js index 793d8880..71f912cb 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -17,6 +17,7 @@ if ( ); } + // if number is 0 else if diff --git a/formatting_test_file.py b/formatting_test_file.py index 1230a838..09f6409e 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -6,6 +6,7 @@ and 2 ==\ 2: + pass From 940237f26b9bcf3bf3ab0f520a8cf5483913cb01 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 10:42:11 +0100 Subject: [PATCH 0042/1000] changed so some steps only work on certain file extensions --- .github/workflows/auto_formatting.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto_formatting.yml b/.github/workflows/auto_formatting.yml index a3114ef0..d4baa807 100644 --- a/.github/workflows/auto_formatting.yml +++ b/.github/workflows/auto_formatting.yml @@ -24,7 +24,7 @@ jobs: # using `black` for Python - name: Format Python code - if: ${{ github.event_path contains '.py' }} + if: git diff --name-only | grep -E '\.py$' uses: docker://python:3.8 with: args: | @@ -32,7 +32,7 @@ jobs: # using `flake8` for Python - name: Lint Python code - if: ${{ github.event_path contains '.py' }} + if: git diff --name-only | grep -E '\.py$' id: python-lint uses: docker://python:3.8 with: @@ -42,7 +42,7 @@ jobs: # using `prettier` for JavaScript - name: Format JavaScript code - if: ${{ github.event_path contains '.js' }} + if: git diff --name-only | grep -E '\.js$' uses: docker://node:16 with: args: | @@ -50,7 +50,7 @@ jobs: # using `eslint` for JavaScript - name: Lint JavaScript code - if: ${{ github.event_path contains '.js' }} + if: git diff --name-only | grep -E '\.js$' id: javascript-lint uses: docker://node:16 with: From 8900325c52c1f01a25279139331c6b486284ee98 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 10:44:23 +0100 Subject: [PATCH 0043/1000] changed so it only looks at files that are changed in the last commit + test files --- .github/workflows/auto_formatting.yml | 8 ++++---- formatting_test_file.js | 1 + formatting_test_file.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto_formatting.yml b/.github/workflows/auto_formatting.yml index d4baa807..ef930547 100644 --- a/.github/workflows/auto_formatting.yml +++ b/.github/workflows/auto_formatting.yml @@ -24,7 +24,7 @@ jobs: # using `black` for Python - name: Format Python code - if: git diff --name-only | grep -E '\.py$' + if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' uses: docker://python:3.8 with: args: | @@ -32,7 +32,7 @@ jobs: # using `flake8` for Python - name: Lint Python code - if: git diff --name-only | grep -E '\.py$' + if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' id: python-lint uses: docker://python:3.8 with: @@ -42,7 +42,7 @@ jobs: # using `prettier` for JavaScript - name: Format JavaScript code - if: git diff --name-only | grep -E '\.js$' + if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' uses: docker://node:16 with: args: | @@ -50,7 +50,7 @@ jobs: # using `eslint` for JavaScript - name: Lint JavaScript code - if: git diff --name-only | grep -E '\.js$' + if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' id: javascript-lint uses: docker://node:16 with: diff --git a/formatting_test_file.js b/formatting_test_file.js index 71f912cb..89a63e37 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -15,6 +15,7 @@ if ( "number does not exist." ); + } diff --git a/formatting_test_file.py b/formatting_test_file.py index 09f6409e..ab24f13a 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -10,6 +10,7 @@ pass + def foo(): print("All the newlines above me should be deleted!") From 8602049c1326ef0b943070149a18b7823c1182ba Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 10:53:01 +0100 Subject: [PATCH 0044/1000] moved if statement to body because it doesn't recognize git in normal if check --- .github/workflows/auto_formatting.yml | 28 +++++++++++++++++++-------- formatting_test_file.js | 1 + formatting_test_file.py | 1 - 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/auto_formatting.yml b/.github/workflows/auto_formatting.yml index ef930547..674009d8 100644 --- a/.github/workflows/auto_formatting.yml +++ b/.github/workflows/auto_formatting.yml @@ -24,38 +24,50 @@ jobs: # using `black` for Python - name: Format Python code - if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' + # if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' uses: docker://python:3.8 with: args: | - sh -c "pip install black && black ." + sh -c " + if git diff --name-only HEAD HEAD^ | grep -E '\.py$'; then + pip install black && black . + fi" # using `flake8` for Python - name: Lint Python code - if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' + # if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' id: python-lint uses: docker://python:3.8 with: args: | - sh -c "pip install flake8 && flake8 ." + sh -c " + if git diff --name-only HEAD HEAD^ | grep -E '\.py$'; then + pip install flake8 && flake8 . + fi" # using `prettier` for JavaScript - name: Format JavaScript code - if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' + # if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' uses: docker://node:16 with: args: | - sh -c "npm install --global prettier && prettier --write '**/*.js'" + sh -c " + if git diff --name-only HEAD HEAD^ | grep -E '\.js$'; then + npm install --global prettier && prettier --write '**/*.js' + fi" # using `eslint` for JavaScript - name: Lint JavaScript code - if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' + # if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' id: javascript-lint uses: docker://node:16 with: args: | - sh -c "npm install --global eslint && eslint '**/*.js'" + sh -c " + if git diff --name-only HEAD HEAD^ | grep -E '\.js$'; then + npm install --global eslint && eslint '**/*.js' + fi" # if either one has a linting error when executing a pull request, give an error and don't allow the request - name: Check for linting errors diff --git a/formatting_test_file.js b/formatting_test_file.js index 89a63e37..fc070947 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -23,6 +23,7 @@ if ( else if + (number === 0) { console.log(`The factorial of ${number} is 1.`); } diff --git a/formatting_test_file.py b/formatting_test_file.py index ab24f13a..09f6409e 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -10,7 +10,6 @@ pass - def foo(): print("All the newlines above me should be deleted!") From 364f5a3b8b9b0ff45a8ce0673b31b4aecf6490ab Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 15:27:13 +0100 Subject: [PATCH 0045/1000] completely restructured the auto-formatting files --- .github/workflows/backend.yml | 46 ++++++++++++++++++++++++++++++++ .github/workflows/frontend.yml | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 .github/workflows/backend.yml create mode 100644 .github/workflows/frontend.yml diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..0fe04519 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,46 @@ +name: Code Formatting and Linting Backend + +on: + # push: + # branches: [ main ] + push: + branches: [ '**' ] + pull_request: + branches: + - main + - develop + +defaults: + run: + working-directory: backend + + +jobs: + format_and_lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + # using `black` for Python + - name: Format Python code + uses: docker://python:3.8 + run: | + pip install black && black . + + # using `flake8` for Python + - name: Lint Python code + id: python-lint + uses: docker://python:3.8 + run: | + pip install flake8 && flake8 . + + - name: Check for linting errors + if: ${{ github.event_name == 'pull_request' }} + run: | + if [ ${{ steps.python-lint.outcome }} == 'failure' ]; then + echo "Linting of code failed. Please fix the errors before merging the pull request." + exit 1 + fi + exit 0 \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..14eb9d99 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,48 @@ +name: Code Formatting and Linting Frontend + +on: + # push: + # branches: [ main ] + push: + branches: [ '**' ] + pull_request: + branches: + - main + - develop + +defaults: + run: + working-directory: frontend + +jobs: + format: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup@v2 + - uses: docker://node:16 + + - name: Format with Prettier + run: | + npm install + npx prettier --check . + RESULT=$? + [ $RESULT -ne 0 ] && exit 1 + exit 0 + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup@v2 + - uses: docker://node:16 + + - name: Lint with ESLint + run: | + rm package-lock.json + npm install + npm run lint + RESULT=$? + [ $RESULT -ne 0 ] && exit 1 + exit 0 From 285d5f59fee31ac78b0b403512af6bd487871d16 Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 15:36:40 +0100 Subject: [PATCH 0046/1000] fixed some things that apparently can't be done in yaml --- .github/workflows/backend.yml | 5 ++--- .github/workflows/frontend.yml | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 0fe04519..340cb0a4 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -21,18 +21,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + - uses: actions/checkout@v2 + - uses: docker://python:3.8 # using `black` for Python - name: Format Python code - uses: docker://python:3.8 run: | pip install black && black . # using `flake8` for Python - name: Lint Python code id: python-lint - uses: docker://python:3.8 run: | pip install flake8 && flake8 . diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 14eb9d99..35f64272 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -20,7 +20,6 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup@v2 - uses: docker://node:16 - name: Format with Prettier @@ -35,7 +34,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup@v2 - uses: docker://node:16 - name: Lint with ESLint From c2db9c63e368ce6ad4ab24a9a4deafcf27a2020a Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 15:52:02 +0100 Subject: [PATCH 0047/1000] yet some other changes to the auto formatting files --- .github/workflows/backend.yml | 22 +++++++++++++--------- .github/workflows/frontend.yml | 10 +++++----- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 340cb0a4..a1e768d4 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -14,26 +14,30 @@ defaults: run: working-directory: backend - jobs: - format_and_lint: + format: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 - uses: docker://python:3.8 - # using `black` for Python - - name: Format Python code + - name: Format with Black run: | - pip install black && black . + pip install black + black . + lint: + runs-on: ubuntu-latest + + steps: + - uses:actions/checkout@v2 + - uses: docker://python:3.8 - # using `flake8` for Python - - name: Lint Python code + - name: Lint with Flake8 id: python-lint run: | - pip install flake8 && flake8 . + pip install flake8 + flake8 . - name: Check for linting errors if: ${{ github.event_name == 'pull_request' }} diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 35f64272..13b3b9e3 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -26,9 +26,9 @@ jobs: run: | npm install npx prettier --check . - RESULT=$? - [ $RESULT -ne 0 ] && exit 1 - exit 0 + # RESULT=$? + # [ $RESULT -ne 0 ] && exit 1 + # exit 0 lint: runs-on: ubuntu-latest @@ -39,8 +39,8 @@ jobs: - name: Lint with ESLint run: | rm package-lock.json - npm install - npm run lint + npm install --global eslint + eslint '**/*.js' RESULT=$? [ $RESULT -ne 0 ] && exit 1 exit 0 From 42bbff9966e61f47d92cd25f270205437839170c Mon Sep 17 00:00:00 2001 From: Sebastiaan Date: Thu, 2 Mar 2023 15:53:50 +0100 Subject: [PATCH 0048/1000] tiny syntax error --- .github/workflows/backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index a1e768d4..095dd798 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses:actions/checkout@v2 + - uses: actions/checkout@v2 - uses: docker://python:3.8 - name: Lint with Flake8 From 5f28d2516fa6e498f989281bcbc79f268a3c24bc Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 00:02:45 +0100 Subject: [PATCH 0049/1000] Initial setup authorisation --- .../migrations => authorisation}/__init__.py | 0 backend/authorisation/permissions.py | 60 +++++++++++++++++++ 2 files changed, 60 insertions(+) rename backend/{authentication/migrations => authorisation}/__init__.py (100%) create mode 100644 backend/authorisation/permissions.py diff --git a/backend/authentication/migrations/__init__.py b/backend/authorisation/__init__.py similarity index 100% rename from backend/authentication/migrations/__init__.py rename to backend/authorisation/__init__.py diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py new file mode 100644 index 00000000..c7f3b238 --- /dev/null +++ b/backend/authorisation/permissions.py @@ -0,0 +1,60 @@ +from rest_framework import permissions + +from base.models import Building + + +# ---------------------- +# ROLE BASED PERMISSIONS +# ---------------------- +class IsAdmin(permissions.BasePermission): + """ + Global permission that only grants access to admin users + """ + message = "Admin permission required" + + def has_permission(self, request, view): + return request.user.role == 'AD' + + +class IsSuperStudent(permissions.BasePermission): + """ + Global permission that grants access to super students + """ + message = "Super student permission required" + + def has_permission(self, request, view): + return request.user.role == 'SS' + + +class IsStudent(permissions.BasePermission): + """ + Global permission that grants access to students + """ + message = "Student permission required" + + def has_permission(self, request, view): + return request.user.role == 'ST' + + +class IsSyndic(permissions.BasePermission): + """ + Global permission that grants access to syndicates + """ + message = "Syndic permission required" + + def has_permission(self, request, view): + return request.user.role == 'SY' + + +# ------------------ +# OBJECT PERMISSIONS +# ------------------ + +class OwnsBuilding(permissions.BasePermission): + """ + Checks if the user owns the building + """ + message = "You must be the owner of the building to be able to perform this request" + + def has_object_permission(self, request, view, obj: Building): + return request.user.id == obj.syndic From 3b8f635c3feae93c636b07b4f37c4095476c463b Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:26:08 +0100 Subject: [PATCH 0050/1000] #18 new approach to autoformatting. split up linting and formatting --- .github/workflows/auto_formatting.yml | 91 ---------------------- .github/workflows/backend.yml | 66 ++++++++-------- .github/workflows/formatting.yml | 35 +++++++++ .github/workflows/frontend.yml | 105 +++++++++++++++----------- .github/workflows/linter.yml | 26 +++++++ 5 files changed, 153 insertions(+), 170 deletions(-) delete mode 100644 .github/workflows/auto_formatting.yml create mode 100644 .github/workflows/formatting.yml create mode 100644 .github/workflows/linter.yml diff --git a/.github/workflows/auto_formatting.yml b/.github/workflows/auto_formatting.yml deleted file mode 100644 index 674009d8..00000000 --- a/.github/workflows/auto_formatting.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Code Formatting and Linting - -on: - push: - branches: [ '**' ] - paths: - - '**.py' - - '**.js' - pull_request: - branches: - - main - - develop - paths: - - '**.py' - - '**.js' - -jobs: - format_and_lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - # using `black` for Python - - name: Format Python code - # if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' - uses: docker://python:3.8 - with: - args: | - sh -c " - if git diff --name-only HEAD HEAD^ | grep -E '\.py$'; then - pip install black && black . - fi" - - # using `flake8` for Python - - name: Lint Python code - # if: git diff --name-only HEAD HEAD^ | grep -E '\.py$' - id: python-lint - uses: docker://python:3.8 - with: - args: | - sh -c " - if git diff --name-only HEAD HEAD^ | grep -E '\.py$'; then - pip install flake8 && flake8 . - fi" - - - # using `prettier` for JavaScript - - name: Format JavaScript code - # if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' - uses: docker://node:16 - with: - args: | - sh -c " - if git diff --name-only HEAD HEAD^ | grep -E '\.js$'; then - npm install --global prettier && prettier --write '**/*.js' - fi" - - # using `eslint` for JavaScript - - name: Lint JavaScript code - # if: git diff --name-only HEAD HEAD^ | grep -E '\.js$' - id: javascript-lint - uses: docker://node:16 - with: - args: | - sh -c " - if git diff --name-only HEAD HEAD^ | grep -E '\.js$'; then - npm install --global eslint && eslint '**/*.js' - fi" - - # if either one has a linting error when executing a pull request, give an error and don't allow the request - - name: Check for linting errors - if: ${{ github.event_name == 'pull_request' }} - run: | - if [ ${{ steps.python-lint.outcome }} == 'failure' -o ${{ steps.javascript-lint.outcome }} == 'failure' ]; then - echo "Linting of code failed. Please fix the errors before merging the pull request." - exit 1 - fi - - # if no linting errors, format code and finalize request - - name: Commit changes - if: ${{ success() }} - run: | - git config --global user.email "group4@selab2.com" - git config --global user.name "Group 4" - if git diff --name-only | grep -E '\.py$|\.js$'; then - git add . - git commit -m "Auto-format and lint code" - git push - fi \ No newline at end of file diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 095dd798..374930b6 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -14,36 +14,36 @@ defaults: run: working-directory: backend -jobs: - format: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: docker://python:3.8 - - - name: Format with Black - run: | - pip install black - black . - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: docker://python:3.8 - - - name: Lint with Flake8 - id: python-lint - run: | - pip install flake8 - flake8 . - - - name: Check for linting errors - if: ${{ github.event_name == 'pull_request' }} - run: | - if [ ${{ steps.python-lint.outcome }} == 'failure' ]; then - echo "Linting of code failed. Please fix the errors before merging the pull request." - exit 1 - fi - exit 0 \ No newline at end of file +#jobs: +# format: +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - uses: docker://python:3.8 +# +# - name: Format with Black +# run: | +# pip install black +# black . +# lint: +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - uses: docker://python:3.8 +# +# - name: Lint with Flake8 +# id: python-lint +# run: | +# pip install flake8 +# flake8 . +# +# - name: Check for linting errors +# if: ${{ github.event_name == 'pull_request' }} +# run: | +# if [ ${{ steps.python-lint.outcome }} == 'failure' ]; then +# echo "Linting of code failed. Please fix the errors before merging the pull request." +# exit 1 +# fi +# exit 0 \ No newline at end of file diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 00000000..1aa4d0f8 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,35 @@ +name: Code Formatting and Linting + + +on: + pull_request: + branches: + - main + - develop + +jobs: + format: + name: Format Code Base + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + # using `black` for Python + - name: Format Python code + uses: rickstaa/action-black@v1.3.1 + with: + black_args: "backend/ --check" + fail_on_error: false + + + # using `prettier` for JavaScript + - name: Format JavaScript code + uses: creyD/prettier_action@v4.3 + with: + prettier_options: -- write frontend/*.{js,tsx} + same_commit: true \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 13b3b9e3..e622ef2d 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,46 +1,59 @@ -name: Code Formatting and Linting Frontend - -on: - # push: - # branches: [ main ] - push: - branches: [ '**' ] - pull_request: - branches: - - main - - develop - -defaults: - run: - working-directory: frontend - -jobs: - format: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: docker://node:16 - - - name: Format with Prettier - run: | - npm install - npx prettier --check . - # RESULT=$? - # [ $RESULT -ne 0 ] && exit 1 - # exit 0 - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: docker://node:16 - - - name: Lint with ESLint - run: | - rm package-lock.json - npm install --global eslint - eslint '**/*.js' - RESULT=$? - [ $RESULT -ne 0 ] && exit 1 - exit 0 +#name: Code Formatting and Linting Frontend +# +#on: +# # push: +# # branches: [ main ] +# push: +# branches: [ '**' ] +# pull_request: +# branches: +# - main +# - develop +# +#defaults: +# run: +# working-directory: frontend +# +#jobs: +# format: +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - uses: docker://node:16 +# +# - name: Format with Prettier +# run: | +# npm install +# npx prettier --check . +# # RESULT=$? +# # [ $RESULT -ne 0 ] && exit 1 +# # exit 0 +# +# lint: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - uses: docker://node:16 +# +# - name: Lint with ESLint +# run: | +# rm package-lock.json +# npm install --global eslint +# eslint '**/*.js' +# RESULT=$? +# [ $RESULT -ne 0 ] && exit 1 +# exit 0 +# +# +# +# - name: Commit changes +# if: ${{ success() }} +# run: | +# git config --global user.email "group4@selab2.com" +# git config --global user.name "Group 4" +# if git diff --name-only | grep -E '\.py$|\.js$'; then +# git add . +# git commit -m "Auto-format and lint code" +# git push +# fi diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000..5e9b63d9 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,26 @@ +name: Lint Code Base + +on: + pull_request: + branches: + - main + - develop + +jobs: + lint: + name: Lint Code Base + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Lint Code Base + uses: docker://github/super-linter@v4.10.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OUTPUT_DETAILS: detailed + VALIDATE_ALL_CODEBASE: false + DEFAULT_BRANCH: main From dd36ff0f10eb3114043c1be59adf5af5121e8e80 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:28:00 +0100 Subject: [PATCH 0051/1000] #18 changed so it works on pull requests on every branch (for testing) --- .github/workflows/formatting.yml | 6 +++--- .github/workflows/linter.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 1aa4d0f8..739e966b 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -3,9 +3,9 @@ name: Code Formatting and Linting on: pull_request: - branches: - - main - - develop + branches: [ '**' ] +# - main +# - develop jobs: format: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5e9b63d9..8e3663fe 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -2,9 +2,9 @@ name: Lint Code Base on: pull_request: - branches: - - main - - develop + branches: [ '**' ] +# - main +# - develop jobs: lint: From ba06e8d2435c18de60fc013549fb8ff24fd34fd9 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:31:35 +0100 Subject: [PATCH 0052/1000] #18 changed so linter works on whole code base --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 8e3663fe..d9e5c813 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -22,5 +22,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OUTPUT_DETAILS: detailed - VALIDATE_ALL_CODEBASE: false +# VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: main From 0d2578714de529888c9b5e9554ce3f0d757930f7 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:35:49 +0100 Subject: [PATCH 0053/1000] #18 changed to work on pushes instead of pull requests. Easier for testing --- .github/workflows/backend.yml | 30 +++++++++++++++--------------- .github/workflows/formatting.yml | 2 +- .github/workflows/linter.yml | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 374930b6..5a76f239 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,18 +1,18 @@ -name: Code Formatting and Linting Backend - -on: - # push: - # branches: [ main ] - push: - branches: [ '**' ] - pull_request: - branches: - - main - - develop - -defaults: - run: - working-directory: backend +#name: Code Formatting and Linting Backend +# +#on: +# # push: +# # branches: [ main ] +# push: +# branches: [ '**' ] +# pull_request: +# branches: +# - main +# - develop +# +#defaults: +# run: +# working-directory: backend #jobs: # format: diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 739e966b..7839288c 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -2,7 +2,7 @@ name: Code Formatting and Linting on: - pull_request: + push: branches: [ '**' ] # - main # - develop diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d9e5c813..9f3875fa 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,7 +1,7 @@ name: Lint Code Base on: - pull_request: + push: branches: [ '**' ] # - main # - develop From 803c1506ab9717f2946178559c371d217b22b2cb Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:40:56 +0100 Subject: [PATCH 0054/1000] #18 deleted old workflows + fixed some issues new ones --- .github/workflows/backend.yml | 49 -------------------------- .github/workflows/formatting.yml | 2 +- .github/workflows/frontend.yml | 59 -------------------------------- .github/workflows/linter.yml | 2 +- 4 files changed, 2 insertions(+), 110 deletions(-) delete mode 100644 .github/workflows/backend.yml delete mode 100644 .github/workflows/frontend.yml diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml deleted file mode 100644 index 5a76f239..00000000 --- a/.github/workflows/backend.yml +++ /dev/null @@ -1,49 +0,0 @@ -#name: Code Formatting and Linting Backend -# -#on: -# # push: -# # branches: [ main ] -# push: -# branches: [ '**' ] -# pull_request: -# branches: -# - main -# - develop -# -#defaults: -# run: -# working-directory: backend - -#jobs: -# format: -# runs-on: ubuntu-latest -# -# steps: -# - uses: actions/checkout@v2 -# - uses: docker://python:3.8 -# -# - name: Format with Black -# run: | -# pip install black -# black . -# lint: -# runs-on: ubuntu-latest -# -# steps: -# - uses: actions/checkout@v2 -# - uses: docker://python:3.8 -# -# - name: Lint with Flake8 -# id: python-lint -# run: | -# pip install flake8 -# flake8 . -# -# - name: Check for linting errors -# if: ${{ github.event_name == 'pull_request' }} -# run: | -# if [ ${{ steps.python-lint.outcome }} == 'failure' ]; then -# echo "Linting of code failed. Please fix the errors before merging the pull request." -# exit 1 -# fi -# exit 0 \ No newline at end of file diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 7839288c..1700e60b 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -31,5 +31,5 @@ jobs: - name: Format JavaScript code uses: creyD/prettier_action@v4.3 with: - prettier_options: -- write frontend/*.{js,tsx} + prettier_options: -- write **/*.{js,tsx} same_commit: true \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml deleted file mode 100644 index e622ef2d..00000000 --- a/.github/workflows/frontend.yml +++ /dev/null @@ -1,59 +0,0 @@ -#name: Code Formatting and Linting Frontend -# -#on: -# # push: -# # branches: [ main ] -# push: -# branches: [ '**' ] -# pull_request: -# branches: -# - main -# - develop -# -#defaults: -# run: -# working-directory: frontend -# -#jobs: -# format: -# runs-on: ubuntu-latest -# -# steps: -# - uses: actions/checkout@v2 -# - uses: docker://node:16 -# -# - name: Format with Prettier -# run: | -# npm install -# npx prettier --check . -# # RESULT=$? -# # [ $RESULT -ne 0 ] && exit 1 -# # exit 0 -# -# lint: -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v2 -# - uses: docker://node:16 -# -# - name: Lint with ESLint -# run: | -# rm package-lock.json -# npm install --global eslint -# eslint '**/*.js' -# RESULT=$? -# [ $RESULT -ne 0 ] && exit 1 -# exit 0 -# -# -# -# - name: Commit changes -# if: ${{ success() }} -# run: | -# git config --global user.email "group4@selab2.com" -# git config --global user.name "Group 4" -# if git diff --name-only | grep -E '\.py$|\.js$'; then -# git add . -# git commit -m "Auto-format and lint code" -# git push -# fi diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 9f3875fa..7b2f8acd 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -18,7 +18,7 @@ jobs: fetch-depth: 0 - name: Lint Code Base - uses: docker://github/super-linter@v4.10.1 + uses: github/super-linter@v4.10.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OUTPUT_DETAILS: detailed From 5a290f9a5d58fbd989b692e8249d1242aa0b8174 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:49:22 +0100 Subject: [PATCH 0055/1000] #18 fix tiny error in formatting file --- .github/workflows/formatting.yml | 4 +- formatting_test_file.js | 26 +++--------- frontend/src/App.js | 70 ++++++++++++++++---------------- frontend/src/App.test.js | 6 +-- frontend/src/index.js | 12 +++--- frontend/src/reportWebVitals.js | 4 +- frontend/src/setupTests.js | 2 +- 7 files changed, 54 insertions(+), 70 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 1700e60b..f59a1726 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,4 +1,4 @@ -name: Code Formatting and Linting +name: Format Code Base on: @@ -31,5 +31,5 @@ jobs: - name: Format JavaScript code uses: creyD/prettier_action@v4.3 with: - prettier_options: -- write **/*.{js,tsx} + prettier_options: --write **/*.{js,tsx} same_commit: true \ No newline at end of file diff --git a/formatting_test_file.js b/formatting_test_file.js index fc070947..0422bbcb 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -2,37 +2,23 @@ // take input from the user -const number - = 5; +const number = 5; // checking if number is negative -if ( - number - < 0) { - console.log("Error! " + - "Factorial" + - " for negative " + - "number does not exist." - +if (number < 0) { + console.log( + "Error! " + "Factorial" + " for negative " + "number does not exist." ); - } - // if number is 0 -else if - - - -(number === 0) { +else if (number === 0) { console.log(`The factorial of ${number} is 1.`); } // if number is positive else { - let fact - - = 1; + let fact = 1; for (let i = 1; i <= number; i++) { fact *= i; } diff --git a/frontend/src/App.js b/frontend/src/App.js index 26e0590e..fc266dc2 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,41 +1,39 @@ -import logo from './logo.svg'; -import './App.css'; -import {useEffect, useState} from "react"; +import logo from "./logo.svg"; +import "./App.css"; +import { useEffect, useState } from "react"; function App() { - - const [data, setData] = useState(null); - - // todo change to https://sel2-4.ugent.be on server - useEffect(() => { - fetch('http://localhost:2002/test/') - .then(res => res.json()) - .then(data => setData(data.data)); - }) - - return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - - - -

JSON data verkregen door api (zie App.js): {data}

- - -
-
- ); + const [data, setData] = useState(null); + + // todo change to https://sel2-4.ugent.be on server + useEffect(() => { + fetch("http://localhost:2002/test/") + .then((res) => res.json()) + .then((data) => setData(data.data)); + }); + + return ( +
+
+ logo +

+ Edit src/App.js and save to reload. +

+ + Learn React + + +

+ JSON data verkregen door api (zie App.js): {data} +

+
+
+ ); } export default App; diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js index 1f03afee..9382b9ad 100644 --- a/frontend/src/App.test.js +++ b/frontend/src/App.test.js @@ -1,7 +1,7 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; +import { render, screen } from "@testing-library/react"; +import App from "./App"; -test('renders learn react link', () => { +test("renders learn react link", () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); diff --git a/frontend/src/index.js b/frontend/src/index.js index d563c0fb..770ee7d6 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,10 +1,10 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; -const root = ReactDOM.createRoot(document.getElementById('root')); +const root = ReactDOM.createRoot(document.getElementById("root")); root.render( diff --git a/frontend/src/reportWebVitals.js b/frontend/src/reportWebVitals.js index 5253d3ad..9ecd33f9 100644 --- a/frontend/src/reportWebVitals.js +++ b/frontend/src/reportWebVitals.js @@ -1,6 +1,6 @@ -const reportWebVitals = onPerfEntry => { +const reportWebVitals = (onPerfEntry) => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js index 8f2609b7..1dd407a6 100644 --- a/frontend/src/setupTests.js +++ b/frontend/src/setupTests.js @@ -2,4 +2,4 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; From 5ab2f42c62df81eb003ea61bba41d848b8a21aad Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 16:49:22 +0100 Subject: [PATCH 0056/1000] #18 fix tiny error in formatting file --- .github/workflows/formatting.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 1700e60b..f59a1726 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,4 +1,4 @@ -name: Code Formatting and Linting +name: Format Code Base on: @@ -31,5 +31,5 @@ jobs: - name: Format JavaScript code uses: creyD/prettier_action@v4.3 with: - prettier_options: -- write **/*.{js,tsx} + prettier_options: --write **/*.{js,tsx} same_commit: true \ No newline at end of file From a9278d43851f68dbb1cb9099287a002a9dcd624a Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 17:11:00 +0100 Subject: [PATCH 0057/1000] #18 changed linter to not check whole code base; changed formatter to commit python changes --- .github/workflows/formatting.yml | 15 ++++++++++++--- .github/workflows/linter.yml | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index f59a1726..6d1cf704 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -22,9 +22,9 @@ jobs: # using `black` for Python - name: Format Python code uses: rickstaa/action-black@v1.3.1 + id: action_black with: - black_args: "backend/ --check" - fail_on_error: false + black_args: "." # using `prettier` for JavaScript @@ -32,4 +32,13 @@ jobs: uses: creyD/prettier_action@v4.3 with: prettier_options: --write **/*.{js,tsx} - same_commit: true \ No newline at end of file + same_commit: true + + - name: Commit Python changes + if: steps.action_black.outputs.is_formatted == 'true' + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + git pull + git commit --amend --no-edit + git push origin -f \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 7b2f8acd..714adee6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -22,5 +22,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OUTPUT_DETAILS: detailed -# VALIDATE_ALL_CODEBASE: false + VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: main From a60ea6377c6af217d1a77d9b3510f5e42fdbfd56 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 17:11:00 +0100 Subject: [PATCH 0058/1000] #18 changed linter to not check whole code base; changed formatter to commit python changes --- .github/workflows/formatting.yml | 15 ++++-- .github/workflows/linter.yml | 2 +- backend/backend/asgi.py | 2 +- backend/backend/settings.py | 90 ++++++++++++++++---------------- backend/backend/urls.py | 5 +- backend/backend/views.py | 6 +-- backend/backend/wsgi.py | 2 +- backend/manage.py | 4 +- formatting_test_file.py | 15 +----- 9 files changed, 67 insertions(+), 74 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index f59a1726..6d1cf704 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -22,9 +22,9 @@ jobs: # using `black` for Python - name: Format Python code uses: rickstaa/action-black@v1.3.1 + id: action_black with: - black_args: "backend/ --check" - fail_on_error: false + black_args: "." # using `prettier` for JavaScript @@ -32,4 +32,13 @@ jobs: uses: creyD/prettier_action@v4.3 with: prettier_options: --write **/*.{js,tsx} - same_commit: true \ No newline at end of file + same_commit: true + + - name: Commit Python changes + if: steps.action_black.outputs.is_formatted == 'true' + run: | + git config --global user.email "group4@selab2.com" + git config --global user.name "Group 4" + git pull + git commit --amend --no-edit + git push origin -f \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 7b2f8acd..714adee6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -22,5 +22,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OUTPUT_DETAILS: detailed -# VALIDATE_ALL_CODEBASE: false + VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: main diff --git a/backend/backend/asgi.py b/backend/backend/asgi.py index f4518a14..023dea82 100644 --- a/backend/backend/asgi.py +++ b/backend/backend/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") application = get_asgi_application() diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 94180df9..c58595e1 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -19,72 +19,72 @@ # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-6jh@t2qxm!3dt5+fgwv%!f6h5))kh3e&m$ec$1d=(*x(rx@)%o' +SECRET_KEY = "django-insecure-6jh@t2qxm!3dt5+fgwv%!f6h5))kh3e&m$ec$1d=(*x(rx@)%o" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['*', 'localhost', '127.0.0.1', '172.17.0.0'] +ALLOWED_HOSTS = ["*", "localhost", "127.0.0.1", "172.17.0.0"] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'corsheaders', - 'rest_framework' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "rest_framework", ] MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] CORS_ALLOW_ALL_ORIGINS = True -ROOT_URLCONF = 'backend.urls' +ROOT_URLCONF = "backend.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'backend.wsgi.application' +WSGI_APPLICATION = "backend.wsgi.application" # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'drtrottoir', - 'USER': 'django', - 'PASSWORD': 'password', + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "drtrottoir", + "USER": "django", + "PASSWORD": "password", # 'HOST': 'localhost' # If you want to run using python manage.py runserver - 'HOST': 'web', # If you want to use `docker-compose up` - 'PORT': '5432', + "HOST": "web", # If you want to use `docker-compose up` + "PORT": "5432", } } @@ -93,25 +93,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -120,9 +120,9 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/backend/backend/urls.py b/backend/backend/urls.py index dd249d2a..a7d02ea9 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -18,7 +18,4 @@ from . import views -urlpatterns = [ - path('admin/', admin.site.urls), - path('test/', views.send_some_data) -] +urlpatterns = [path("admin/", admin.site.urls), path("test/", views.send_some_data)] diff --git a/backend/backend/views.py b/backend/backend/views.py index 535c7c2b..7d03f84b 100644 --- a/backend/backend/views.py +++ b/backend/backend/views.py @@ -2,9 +2,7 @@ from rest_framework.response import Response -@api_view(['GET']) +@api_view(["GET"]) def send_some_data(request): # In urls.py, this function is mapped with '/test' - return Response({ - "data": "Hello from django backend" - }) + return Response({"data": "Hello from django backend"}) diff --git a/backend/backend/wsgi.py b/backend/backend/wsgi.py index c4aa3242..7d6cf063 100644 --- a/backend/backend/wsgi.py +++ b/backend/backend/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") application = get_wsgi_application() diff --git a/backend/manage.py b/backend/manage.py index eb6431e2..1917e46e 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/formatting_test_file.py b/formatting_test_file.py index 09f6409e..2739d124 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,27 +1,16 @@ -j = [1, - 2, - 3] - -if 1 == 1 \ - and 2 ==\ - 2: - +j = [1, 2, 3] +if 1 == 1 and 2 == 2: pass def foo(): - print("All the newlines above me should be deleted!") if True: - - - print("No newline above me!") - print("There is a newline above me, and that's OK!") From 8fd410ad26bdb7865c8218bbce602f46670f4186 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 17:22:05 +0100 Subject: [PATCH 0059/1000] #18 moved formatting and linting to same file so they are executed sequentially --- .github/workflows/formatting.yml | 9 ++++++++- .github/workflows/linter.yml | 26 -------------------------- 2 files changed, 8 insertions(+), 27 deletions(-) delete mode 100644 .github/workflows/linter.yml diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 6d1cf704..1b035cc1 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -41,4 +41,11 @@ jobs: git config --global user.name "Group 4" git pull git commit --amend --no-edit - git push origin -f \ No newline at end of file + git push origin -f + + - name: Lint Code Base + uses: github/super-linter@v4.10.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATE_ALL_CODEBASE: false + DEFAULT_BRANCH: main \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml deleted file mode 100644 index 714adee6..00000000 --- a/.github/workflows/linter.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Lint Code Base - -on: - push: - branches: [ '**' ] -# - main -# - develop - -jobs: - lint: - name: Lint Code Base - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Lint Code Base - uses: github/super-linter@v4.10.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OUTPUT_DETAILS: detailed - VALIDATE_ALL_CODEBASE: false - DEFAULT_BRANCH: main From f7b7678ed61b1cb2bc3ff7901cd42c34aef023d4 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 17:31:14 +0100 Subject: [PATCH 0060/1000] #18 do some tests --- .github/workflows/formatting.yml | 9 +++++---- formatting_test_file.js | 16 ++++++++++++---- formatting_test_file.py | 17 +++++++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 1b035cc1..50a82f53 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,4 +1,4 @@ -name: Format Code Base +name: Format and Lint Code Base on: @@ -8,8 +8,8 @@ on: # - develop jobs: - format: - name: Format Code Base + build: + name: Format and Lint Code Base runs-on: ubuntu-latest steps: @@ -48,4 +48,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false - DEFAULT_BRANCH: main \ No newline at end of file + DEFAULT_BRANCH: main + VALIDATE_YAML: false \ No newline at end of file diff --git a/formatting_test_file.js b/formatting_test_file.js index 0422bbcb..dad27532 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -2,22 +2,30 @@ // take input from the user -const number = 5; +const number = + 5; // checking if number is negative if (number < 0) { console.log( - "Error! " + "Factorial" + " for negative " + "number does not exist." + + + "Error! " + "Factorial" + " for negative " + "number does not exist." ); } // if number is 0 -else if (number === 0) { - console.log(`The factorial of ${number} is 1.`); +else if (number === 0) { + console.log(`The factorial + of ${number} is 1.`); } // if number is positive + + else { + + let fact = 1; for (let i = 1; i <= number; i++) { fact *= i; diff --git a/formatting_test_file.py b/formatting_test_file.py index 2739d124..383bc8b6 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,17 +1,26 @@ -j = [1, 2, 3] +j = [1, + 2, + 3] -if 1 == 1 and 2 == 2: +if 1 ==1 \ + and 2 == 2: pass def foo(): - print("All the newlines above me should be deleted!") + + + print("All " + "the newlines above me should be deleted!") if True: print("No newline above me!") - print("There is a newline above me, and that's OK!") + + print("There is a newline above " + "" + "me, and that's OK!") class Point: From f12e2a7264c96f1e11024d0f6c3844c6682abdfb Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 17:31:14 +0100 Subject: [PATCH 0061/1000] #18 do some tests --- .github/workflows/formatting.yml | 9 +++++---- formatting_test_file.js | 3 ++- formatting_test_file.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 1b035cc1..50a82f53 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,4 +1,4 @@ -name: Format Code Base +name: Format and Lint Code Base on: @@ -8,8 +8,8 @@ on: # - develop jobs: - format: - name: Format Code Base + build: + name: Format and Lint Code Base runs-on: ubuntu-latest steps: @@ -48,4 +48,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false - DEFAULT_BRANCH: main \ No newline at end of file + DEFAULT_BRANCH: main + VALIDATE_YAML: false \ No newline at end of file diff --git a/formatting_test_file.js b/formatting_test_file.js index 0422bbcb..ff92c548 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -13,7 +13,8 @@ if (number < 0) { // if number is 0 else if (number === 0) { - console.log(`The factorial of ${number} is 1.`); + console.log(`The factorial + of ${number} is 1.`); } // if number is positive diff --git a/formatting_test_file.py b/formatting_test_file.py index 2739d124..ba5f08b7 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -5,13 +5,13 @@ def foo(): - print("All the newlines above me should be deleted!") + print("All " "the newlines above me should be deleted!") if True: print("No newline above me!") - print("There is a newline above me, and that's OK!") + print("There is a newline above " "" "me, and that's OK!") class Point: From 050b29ecbe2dda023b6e97ade188b7750f73b34e Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 17:08:11 +0000 Subject: [PATCH 0062/1000] Auto formatted JS code --- formatting_test_file.js | 4 +--- formatting_test_file.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/formatting_test_file.js b/formatting_test_file.js index 8d7620e1..ff92c548 100644 --- a/formatting_test_file.js +++ b/formatting_test_file.js @@ -2,9 +2,7 @@ // take input from the user -const number = - - 5; +const number = 5; // checking if number is negative if (number < 0) { diff --git a/formatting_test_file.py b/formatting_test_file.py index 0ab2e16a..ba5f08b7 100644 --- a/formatting_test_file.py +++ b/formatting_test_file.py @@ -1,7 +1,4 @@ -j = [1, - 2, - - 3] +j = [1, 2, 3] if 1 == 1 and 2 == 2: pass From 03af60ab43022e9a4a128bc809dd15051d1e59b6 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Tue, 14 Mar 2023 18:15:15 +0100 Subject: [PATCH 0063/1000] #18 removed redundant commit of python code; rescructured linter --- .github/workflows/formatting.yml | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 0dad19d4..237fbb2e 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -7,7 +7,7 @@ on: # - develop jobs: - build: + format: name: Format Code Base runs-on: ubuntu-latest @@ -25,23 +25,29 @@ jobs: with: black_args: "." - # using `prettier` for JavaScript - name: Format JavaScript code uses: creyD/prettier_action@v4.3 with: prettier_options: --write **/*.{js,tsx} - commit_message: "Auto formatted JS code" + commit_message: "Auto formatted code" only_changed: true github_token: ${{ secrets.GITHUB_TOKEN }} + lint: + needs: [format] + name: Lint Code Base + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 - - name: Commit changes - if: steps.action_black.outputs.is_formatted == 'true' - run: | - git config --global user.email "group4@selab2.com" - git config --global user.name "Group 4" - git pull - git add **/*.py - git commit -m "Auto formatted Python code" - git push \ No newline at end of file + - name: Lint Code Base + uses: github/super-linter@v4 + env: + VALIDATE_ALL_CODEBASE: false + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From af06b853acd79f4ff1f7dcb65e9ff1115f3ad58c Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 19:49:31 +0100 Subject: [PATCH 0064/1000] Setup authorisations for buildings --- backend/authorisation/permissions.py | 32 ++++++++++++++++++++-------- backend/building/views.py | 18 +++++++++++++--- backend/config/settings.py | 2 +- readme/authorisation.md | 22 +++++++++++++++++++ 4 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 readme/authorisation.md diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index c7f3b238..3c43bca0 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -1,12 +1,12 @@ -from rest_framework import permissions - +from rest_framework.permissions import BasePermission +from rest_framework.permissions import SAFE_METHODS from base.models import Building # ---------------------- # ROLE BASED PERMISSIONS # ---------------------- -class IsAdmin(permissions.BasePermission): +class IsAdmin(BasePermission): """ Global permission that only grants access to admin users """ @@ -16,7 +16,7 @@ def has_permission(self, request, view): return request.user.role == 'AD' -class IsSuperStudent(permissions.BasePermission): +class IsSuperStudent(BasePermission): """ Global permission that grants access to super students """ @@ -26,7 +26,7 @@ def has_permission(self, request, view): return request.user.role == 'SS' -class IsStudent(permissions.BasePermission): +class IsStudent(BasePermission): """ Global permission that grants access to students """ @@ -36,7 +36,18 @@ def has_permission(self, request, view): return request.user.role == 'ST' -class IsSyndic(permissions.BasePermission): +class ReadOnlyStudent(BasePermission): + """ + Global permission that only grants read access for students + """ + message = "Students are only allowed to read" + + def has_permission(self, request, view): + if request.method in SAFE_METHODS: + return request.user.role == 'ST' + + +class IsSyndic(BasePermission): """ Global permission that grants access to syndicates """ @@ -50,11 +61,14 @@ def has_permission(self, request, view): # OBJECT PERMISSIONS # ------------------ -class OwnsBuilding(permissions.BasePermission): +class OwnerOfBuilding(BasePermission): """ Checks if the user owns the building """ - message = "You must be the owner of the building to be able to perform this request" + message = "You can only access the buildings that you own" + + def has_permission(self, request, view): + return request.user.role == 'SY' def has_object_permission(self, request, view, obj: Building): - return request.user.id == obj.syndic + return request.user.id == obj.syndic_id diff --git a/backend/building/views.py b/backend/building/views.py index e06d30eb..a10cf0ac 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -1,13 +1,14 @@ -from rest_framework import permissions +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import OwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent, IsSyndic from base.models import Building from base.serializers import BuildingSerializer from util.request_response_util import * class DefaultBuilding(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def post(self, request): """ @@ -32,7 +33,9 @@ def post(self, request): class BuildingIndividualView(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, + IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding + ] def get(self, request, building_id): """ @@ -44,6 +47,7 @@ def get(self, request, building_id): return bad_request(object_name="Building") building_instance = building_instance[0] + self.check_object_permissions(request, building_instance) serializer = BuildingSerializer(building_instance) return get_succes(serializer) @@ -56,6 +60,8 @@ def delete(self, request, building_id): return bad_request(object_name="building") building_instance = building_instance[0] + self.check_object_permissions(request, building_instance) + building_instance.delete() return delete_succes() @@ -68,6 +74,7 @@ def patch(self, request, building_id): return bad_request(object_name="building") building_instance = building_instance[0] + self.check_object_permissions(request, building_instance) data = request_to_dict(request.data) if "syndic" in data.keys(): @@ -84,6 +91,7 @@ def patch(self, request, building_id): class AllBuildingsView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def get(self, request): """ @@ -96,6 +104,7 @@ def get(self, request): class BuildingOwnerView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding] def get(self, request, owner_id): """ @@ -103,6 +112,9 @@ def get(self, request, owner_id): """ building_instance = Building.objects.filter(syndic=owner_id) + for b in building_instance: + self.check_object_permissions(request, b) + if not building_instance: return bad_request_relation("building", "syndic") diff --git a/backend/config/settings.py b/backend/config/settings.py index 8b4f7958..6bb1a39f 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -91,7 +91,7 @@ } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=100), 'REFRESH_TOKEN_LIFETIME': timedelta(days=14), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, diff --git a/readme/authorisation.md b/readme/authorisation.md new file mode 100644 index 00000000..1f9636d4 --- /dev/null +++ b/readme/authorisation.md @@ -0,0 +1,22 @@ +# Authorisation + +## Overview + +### Role based permissions + +- `IsAdmin` (global): checks if the user is an admin +- `IsSuperStudent` (global): checks if the user is a super student +- `IsStudent` (global): checks if the user is a student +- `ReadOnlyStudent` (global): checks if the user is a student and only performs a `GET/HEAD/OPTIONS` request +- `IsSyndic` (global): checks if the user is a syndic + +### Object based permissions + +- `OwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building + +## Protected endpoints +For all these views, `IsAuthenticated` is required. Therefor we only mention the interesting permissions here. +### Building urls +- `building/ - [..., IsAdmin|IsSuperStudent]` +- `building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding]` +- `building/owner/id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding]` From 5128226f3898824897b401f0ac28c134d1738427 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 20:26:43 +0100 Subject: [PATCH 0065/1000] authorisation authentication --- backend/authentication/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/authentication/views.py b/backend/authentication/views.py index f245a2ba..0f8a6fa7 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -3,6 +3,7 @@ from dj_rest_auth.views import LogoutView, LoginView from django.utils.translation import gettext_lazy as _ from rest_framework import status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework_simplejwt.exceptions import TokenError from rest_framework_simplejwt.tokens import RefreshToken @@ -12,6 +13,8 @@ class LogoutViewWithBlacklisting(LogoutView): + permission_classes = [IsAuthenticated] + def logout(self, request): response = Response( {'detail': _('Successfully logged out.')}, @@ -45,6 +48,7 @@ def logout(self, request): class RefreshViewHiddenTokens(TokenRefreshView): + permission_classes = [IsAuthenticated] serializer_class = CookieTokenRefreshSerializer def finalize_response(self, request, response, *args, **kwargs): From 6b43f596281c30ffa0516cc4582d09892f89010c Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 20:26:56 +0100 Subject: [PATCH 0066/1000] fixing loginview --- backend/base/serializers.py | 2 +- backend/config/settings.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/base/serializers.py b/backend/base/serializers.py index 6770479d..a40f5e0c 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -7,7 +7,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ["id", "username", "email", "first_name", "last_name", + fields = ["id", "email", "first_name", "last_name", "phone_number", "region", "role"] read_only_fields = ["id", "email"] diff --git a/backend/config/settings.py b/backend/config/settings.py index 6bb1a39f..f393301c 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -55,7 +55,19 @@ ] CREATED_APPS = [ - 'base' + 'authentication', + 'authorisation', + 'base', + 'building', + 'building_on_tour', + 'buildingurl', + 'garbage_collection', + 'manual', + 'picture_building', + 'region', + 'student_at_building_on_tour', + 'tour', + 'users', ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CREATED_APPS @@ -70,7 +82,7 @@ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } -#drf-spectacular settings +# drf-spectacular settings SPECTACULAR_SETTINGS = { 'TITLE': 'Dr-Trottoir API', 'DESCRIPTION': 'This is the documentation for the Dr-trottoir API', @@ -88,6 +100,7 @@ 'JWT_AUTH_SAMESITE': 'Strict', 'JWT_AUTH_COOKIE': 'auth-access-token', 'JWT_AUTH_REFRESH_COOKIE': 'auth-refresh-token', + 'USER_DETAILS_SERIALIZER': 'base.serializers.UserSerializer', } SIMPLE_JWT = { From bf82fe24cddec77818b03cf7b0490f21a51cff7f Mon Sep 17 00:00:00 2001 From: sevrijss Date: Tue, 14 Mar 2023 22:06:40 +0100 Subject: [PATCH 0067/1000] setup coverage testing for django. Command is show in picture in ./readme/img/example_coverage.jpg Currently it doesn't work yet in the docker itself --- backend/config/settings.py | 13 ++++++++-- backend/region/tests.py | 43 +++++++++++++++++++++++++++++++- backend/requirements.txt | 4 ++- readme/img/example_coverage.jpg | Bin 0 -> 31095 bytes 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 readme/img/example_coverage.jpg diff --git a/backend/config/settings.py b/backend/config/settings.py index 8b4f7958..4650901a 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -9,6 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ +import collections from datetime import timedelta from pathlib import Path from .secrets import DJANGO_SECRET_KEY, SECRET_EMAIL_USER, SECRET_EMAIL_USER_PSWD @@ -35,6 +36,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', + 'django_nose', ] AUTHENTICATION = [ @@ -70,6 +72,13 @@ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } +# hack to make nose run +# this is needed because a lib was updated +# https://stackoverflow.com/a/70641487 +collections.Callable = collections.abc.Callable +# Use nose to run all tests +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + #drf-spectacular settings SPECTACULAR_SETTINGS = { 'TITLE': 'Dr-Trottoir API', @@ -157,8 +166,8 @@ 'NAME': 'drtrottoir', 'USER': 'django', 'PASSWORD': 'password', - # 'HOST': 'localhost', # If you want to run using python manage.py runserver - 'HOST': 'web', # If you want to use `docker-compose up` + 'HOST': 'localhost', # If you want to run using python manage.py runserver + # 'HOST': 'web', # If you want to use `docker-compose up` 'PORT': '5432', } } diff --git a/backend/region/tests.py b/backend/region/tests.py index 7ce503c2..5c19a0d5 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -1,3 +1,44 @@ from django.test import TestCase +from rest_framework.test import APIClient -# Create your tests here. +from base.models import User + + +def createUser(is_staff: bool = True) -> User: + user = User( + first_name="test", + last_name="test", + email="test@test.com", + is_staff=is_staff, + is_active=True, + phone_number="+32485710347", + role="AD" + ) + user.save() + return user + + +class RegionTests(TestCase): + def test_empty_region_list(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + response = client.get("http://localhost:2002/region/all", follow=True) + assert response.status_code == 200 + data = [response.data[e] for e in response.data] + assert len(data) == 0 + + def test_insert_1_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data = { + "region": "Gent" + } + response = client.post("http://localhost:2002/region/", data, follow=True) + assert response.status_code == 201 + for key in data: + # alle info zou er in moeten zitten + assert key in response.data + # er moet ook een id bij zitten + assert "id" in response.data diff --git a/backend/requirements.txt b/backend/requirements.txt index e9594f09..fba1dc31 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -28,4 +28,6 @@ six==1.16.0 sqlparse==0.4.3 urllib3==1.26.14 Pillow==9.4.0 -drf-spectacular==0.26.0 \ No newline at end of file +drf-spectacular==0.26.0 +django-nose==1.4.7 +coverage==7.2.1 \ No newline at end of file diff --git a/readme/img/example_coverage.jpg b/readme/img/example_coverage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d98553aa4c3334538699b030627dda27d04605f8 GIT binary patch literal 31095 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 SXWb(I6@Q-p0yoRAL%#tgBFDP` literal 0 HcmV?d00001 From a26d34a3566e0c6146af03d15f0711464cd4dfd8 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 22:29:03 +0100 Subject: [PATCH 0068/1000] removing redundant comment --- backend/base/models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index d414c82e..dc864ea4 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -18,13 +18,6 @@ def __str__(self): return self.region -# Catches the post_save signal (in signals.py) and creates a user token if not yet created -# @receiver(post_save, sender=settings.AUTH_USER_MODEL) -# def create_auth_token(sender, instance=None, created=False, **kwargs): -# if created: -# Token.objects.create(user=instance) - - class User(AbstractBaseUser, PermissionsMixin): username = None # extra fields for authentication From 641835e99b44ee689eadfe535bd10f7ce79952c3 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 22:36:19 +0100 Subject: [PATCH 0069/1000] authorisation user --- backend/authorisation/permissions.py | 30 +++++++++++++++++++++++++--- backend/users/views.py | 25 +++++++++++++---------- readme/authorisation.md | 1 + 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 3c43bca0..340ea037 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -1,6 +1,7 @@ from rest_framework.permissions import BasePermission -from rest_framework.permissions import SAFE_METHODS -from base.models import Building +from base.models import Building, User + +SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] # ---------------------- @@ -65,10 +66,33 @@ class OwnerOfBuilding(BasePermission): """ Checks if the user owns the building """ - message = "You can only access the buildings that you own" + message = "You can only access/edit the buildings that you own" def has_permission(self, request, view): return request.user.role == 'SY' def has_object_permission(self, request, view, obj: Building): return request.user.id == obj.syndic_id + + +class OwnsAccount(BasePermission): + """ + Checks if the user is owns the user account + """ + message = "You can only access/edit your own account" + + def has_object_permission(self, request, view, obj: User): + if request.method in SAFE_METHODS + ['PATCH']: + return request.user.id == obj.id + + +class CanEditUser(BasePermission): + """ + Checks if the user has the right permissions to edit + """ + message = "You don't have the right permissions to edit the user accordingly" + + def has_object_permission(self, request, view, obj: User): + if request.method in ['PATCH', 'DELETE']: + # TODO: Depending on the 'Rank' of the 'Role', we should determine this permission + return True diff --git a/backend/users/views.py b/backend/users/views.py index d6e84488..247e988a 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,8 +1,9 @@ import json -from rest_framework import permissions +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnsAccount, CanEditUser from base.models import User from base.serializers import UserSerializer from util.request_response_util import * @@ -17,9 +18,7 @@ def _try_adding_region_to_user_instance(user_instance, region_value): class DefaultUser(APIView): - - # TODO: authorization - # permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] # TODO: in order for this to work, you have to pass a password # In the future, we probably won't use POST this way anymore (if we work with the whitelist method) @@ -41,9 +40,6 @@ def post(self, request): user_instance.save() # Now that we have an ID, we can look at the many-to-many relationship region - - print("beginnen met REGIONNNNNNN ") - if "region" in data.keys(): region_dict = json.loads(data["region"]) for value in region_dict.values(): @@ -58,7 +54,7 @@ def post(self, request): class UserIndividualView(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser] def get(self, request, user_id): """ @@ -68,8 +64,11 @@ def get(self, request, user_id): user_instance = User.objects.filter(id=user_id) if not user_instance: return bad_request(object_name="User") + user_instance = user_instance[0] + + self.check_object_permissions(request, user_instance) - serializer = UserSerializer(user_instance[0]) + serializer = UserSerializer(user_instance) return get_succes(serializer) def delete(self, request, user_id): @@ -79,8 +78,11 @@ def delete(self, request, user_id): user_instance = User.objects.filter(id=user_id) if not user_instance: return bad_request(object_name="User") + user_instance = user_instance[0] + + self.check_object_permissions(request, user_instance) - user_instance[0].delete() + user_instance.delete() return delete_succes() def patch(self, request, user_id): @@ -93,6 +95,8 @@ def patch(self, request, user_id): return bad_request(object_name="User") user_instance = user_instance[0] + self.check_object_permissions(request, user_instance) + data = request_to_dict(request.data) for key in data.keys(): @@ -118,6 +122,7 @@ def patch(self, request, user_id): class AllUsersView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def get(self, request): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index 1f9636d4..d8afbcd1 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -13,6 +13,7 @@ ### Object based permissions - `OwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building +- `OwnsAccount` (object): checks if the user tries to access his own user info ## Protected endpoints For all these views, `IsAuthenticated` is required. Therefor we only mention the interesting permissions here. From e65f773b9c671a1df7b87f9e75aa5e0d908a96d5 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 22:38:28 +0100 Subject: [PATCH 0070/1000] authorisation user - documentation --- readme/authorisation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme/authorisation.md b/readme/authorisation.md index d8afbcd1..426a19ad 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -14,10 +14,15 @@ - `OwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building - `OwnsAccount` (object): checks if the user tries to access his own user info +- **TODO** `CanEditUser` (object): checks if the user who tries to edit the user has the right permissions (higher rank, doesn't + edit the rolt to a higher rank than itself, ...) ## Protected endpoints + For all these views, `IsAuthenticated` is required. Therefor we only mention the interesting permissions here. + ### Building urls + - `building/ - [..., IsAdmin|IsSuperStudent]` - `building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding]` - `building/owner/id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding]` From 61f05d4ded115a488469137ef201af713c3ab5ba Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 14 Mar 2023 23:06:26 +0100 Subject: [PATCH 0071/1000] update docs user endpoints + authorisation tours --- backend/tour/views.py | 14 +++++++++----- readme/authorisation.md | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/backend/tour/views.py b/backend/tour/views.py index 68f8cc1c..51eeda8e 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -1,14 +1,14 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView + +from authorisation.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent from base.models import Tour, Region from base.serializers import TourSerializer -from rest_framework import permissions -from rest_framework import status -from rest_framework.response import Response -from rest_framework.views import APIView from util.request_response_util import * class Default(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def post(self, request): """ @@ -30,6 +30,8 @@ def post(self, request): class TourIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] + def get(self, request, tour_id): """ Get info about a Tour with given id @@ -81,6 +83,8 @@ def delete(self, request, tour_id): class AllToursView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + def get(self, request): """ Get all tours diff --git a/readme/authorisation.md b/readme/authorisation.md index 426a19ad..ce839fc5 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -14,7 +14,8 @@ - `OwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building - `OwnsAccount` (object): checks if the user tries to access his own user info -- **TODO** `CanEditUser` (object): checks if the user who tries to edit the user has the right permissions (higher rank, doesn't +- **TODO** `CanEditUser` (object): checks if the user who tries to edit the user has the right permissions (higher rank, + doesn't edit the rolt to a higher rank than itself, ...) ## Protected endpoints @@ -26,3 +27,16 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `building/ - [..., IsAdmin|IsSuperStudent]` - `building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding]` - `building/owner/id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding]` +- `building/all - [...,IsAdmin | IsSuperStudent]` + +### User urls + +- `user/ - [..., IsAdmin | IsSuperStudent]` +- `user/id - [..., IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser]` +- `user/all - [..., IsAdmin | IsSuperStudent]` + +### Tour urls + +- `tour/ - [..., IsAdmin | IsSuperStudent]` +- `tour/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` +- `tour/all - [..., IsAdmin | IsSuperStudent]` \ No newline at end of file From 9e340d9c717fc903eba6d705a2e616f416feec83 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Wed, 15 Mar 2023 13:55:10 +0100 Subject: [PATCH 0072/1000] insert multiple regions test --- backend/region/tests.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/region/tests.py b/backend/region/tests.py index 5c19a0d5..6b9e14c3 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -42,3 +42,30 @@ def test_insert_1_region(self): assert key in response.data # er moet ook een id bij zitten assert "id" in response.data + + def test_insert_multiple_regions(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data1 = { + "region": "Gent" + } + data2 = { + "region": "Antwerpen" + } + response1 = client.post("http://localhost:2002/region/", data1, follow=True) + response2 = client.post("http://localhost:2002/region/", data2, follow=True) + assert response1.status_code == 201 + assert response2.status_code == 201 + for key in data1: + # alle info zou er in moeten zitten + assert key in response1.data + for key in data2: + # alle info zou er in moeten zitten + assert key in response2.data + # er moet ook een id bij zitten + assert "id" in response1.data + assert "id" in response2.data + results = client.get("/region/all/") + print([r for r in results.data]) + assert False From 1d4fa1a5534d1e33160ed3ad5fff200c08ef987c Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 19:04:43 +0100 Subject: [PATCH 0073/1000] Fixing user permissions with latest updated roles --- backend/authorisation/permissions.py | 38 +++++++++++++++++++--------- backend/users/views.py | 4 +-- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 340ea037..a37d03a1 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -1,5 +1,6 @@ from rest_framework.permissions import BasePermission -from base.models import Building, User +from base.models import Building, User, Role +from util.request_response_util import request_to_dict SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] @@ -14,7 +15,7 @@ class IsAdmin(BasePermission): message = "Admin permission required" def has_permission(self, request, view): - return request.user.role == 'AD' + return request.user.role.role.lower() == 'admin' class IsSuperStudent(BasePermission): @@ -24,7 +25,7 @@ class IsSuperStudent(BasePermission): message = "Super student permission required" def has_permission(self, request, view): - return request.user.role == 'SS' + return request.user.role.role.lower() == 'superstudent' class IsStudent(BasePermission): @@ -34,7 +35,7 @@ class IsStudent(BasePermission): message = "Student permission required" def has_permission(self, request, view): - return request.user.role == 'ST' + return request.user.role.role.lower() == 'student' class ReadOnlyStudent(BasePermission): @@ -45,7 +46,7 @@ class ReadOnlyStudent(BasePermission): def has_permission(self, request, view): if request.method in SAFE_METHODS: - return request.user.role == 'ST' + return request.user.role.role.lower() == 'student' class IsSyndic(BasePermission): @@ -55,7 +56,7 @@ class IsSyndic(BasePermission): message = "Syndic permission required" def has_permission(self, request, view): - return request.user.role == 'SY' + return request.user.role.role.lower() == 'syndic' # ------------------ @@ -69,7 +70,7 @@ class OwnerOfBuilding(BasePermission): message = "You can only access/edit the buildings that you own" def has_permission(self, request, view): - return request.user.role == 'SY' + return request.user.role.role.lower() == 'syndic' def has_object_permission(self, request, view, obj: Building): return request.user.id == obj.syndic_id @@ -82,8 +83,7 @@ class OwnsAccount(BasePermission): message = "You can only access/edit your own account" def has_object_permission(self, request, view, obj: User): - if request.method in SAFE_METHODS + ['PATCH']: - return request.user.id == obj.id + return request.user.id == obj.id class CanEditUser(BasePermission): @@ -93,6 +93,20 @@ class CanEditUser(BasePermission): message = "You don't have the right permissions to edit the user accordingly" def has_object_permission(self, request, view, obj: User): - if request.method in ['PATCH', 'DELETE']: - # TODO: Depending on the 'Rank' of the 'Role', we should determine this permission - return True + if request.method == 'PATCH': + return request.user.id == obj.id or request.user.role.rank < obj.role.rank + return True + + +class CanEditRole(BasePermission): + """ + Checks if the user has the right permissions to edit the role of a user + """ + message = "You can't assign a role that is higher that your own" + + def has_object_permission(self, request, view, obj: User): + if request.method in ['PATCH']: + data = request_to_dict(request.data) + role_instance = Role.objects.filter(id=data['role'])[0] + return request.user.role.rank <= role_instance.rank + return True diff --git a/backend/users/views.py b/backend/users/views.py index 3c0e5fec..06b96a8b 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import IsAdmin, IsSuperStudent, OwnsAccount, CanEditUser +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnsAccount, CanEditUser, CanEditRole from base.models import User from base.serializers import UserSerializer from util.request_response_util import * @@ -58,7 +58,7 @@ def post(self, request): class UserIndividualView(APIView): - permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser, CanEditRole] def get(self, request, user_id): """ From 87c148aa36f4e183c279138b200ca9465bfc165e Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 19:12:15 +0100 Subject: [PATCH 0074/1000] Changing 'role.role' to 'role.name' in permissionclasses --- backend/authorisation/permissions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index a37d03a1..6f696ad7 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -15,7 +15,7 @@ class IsAdmin(BasePermission): message = "Admin permission required" def has_permission(self, request, view): - return request.user.role.role.lower() == 'admin' + return request.user.role.name.lower() == 'admin' class IsSuperStudent(BasePermission): @@ -25,7 +25,7 @@ class IsSuperStudent(BasePermission): message = "Super student permission required" def has_permission(self, request, view): - return request.user.role.role.lower() == 'superstudent' + return request.user.role.name.lower() == 'superstudent' class IsStudent(BasePermission): @@ -35,7 +35,7 @@ class IsStudent(BasePermission): message = "Student permission required" def has_permission(self, request, view): - return request.user.role.role.lower() == 'student' + return request.user.role.name.lower() == 'student' class ReadOnlyStudent(BasePermission): @@ -46,7 +46,7 @@ class ReadOnlyStudent(BasePermission): def has_permission(self, request, view): if request.method in SAFE_METHODS: - return request.user.role.role.lower() == 'student' + return request.user.role.name.lower() == 'student' class IsSyndic(BasePermission): @@ -56,7 +56,7 @@ class IsSyndic(BasePermission): message = "Syndic permission required" def has_permission(self, request, view): - return request.user.role.role.lower() == 'syndic' + return request.user.role.name.lower() == 'syndic' # ------------------ @@ -70,7 +70,7 @@ class OwnerOfBuilding(BasePermission): message = "You can only access/edit the buildings that you own" def has_permission(self, request, view): - return request.user.role.role.lower() == 'syndic' + return request.user.role.name.lower() == 'syndic' def has_object_permission(self, request, view, obj: Building): return request.user.id == obj.syndic_id From 20c00242fa3cac55063d08b0b3313363069eefd8 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 19:22:47 +0100 Subject: [PATCH 0075/1000] Buildings can only be deleted by superstudents/admins --- backend/authorisation/permissions.py | 4 +++- readme/authorisation.md | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 6f696ad7..1c78c71d 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -73,7 +73,9 @@ def has_permission(self, request, view): return request.user.role.name.lower() == 'syndic' def has_object_permission(self, request, view, obj: Building): - return request.user.id == obj.syndic_id + if request.method in SAFE_METHODS + ['PATCH']: + return request.user.id == obj.syndic_id + return False class OwnsAccount(BasePermission): diff --git a/readme/authorisation.md b/readme/authorisation.md index ce839fc5..42e148f7 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -14,9 +14,8 @@ - `OwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building - `OwnsAccount` (object): checks if the user tries to access his own user info -- **TODO** `CanEditUser` (object): checks if the user who tries to edit the user has the right permissions (higher rank, - doesn't - edit the rolt to a higher rank than itself, ...) +- `CanEditUser` (object): checks if the user who tries to edit is in fact the user himself or someone with a higher rank +- `CanEditRole` (object): checks if the user who tries to assign a role, doesn't set a role higher than his own role ## Protected endpoints @@ -32,7 +31,7 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` -- `user/id - [..., IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser]` +- `user/id - [..., IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser, CanEditRole]` - `user/all - [..., IsAdmin | IsSuperStudent]` ### Tour urls From f449720b13b15a283182261d41b7c13dedb502f4 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 19:35:26 +0100 Subject: [PATCH 0076/1000] syndic can only read building --- backend/authorisation/permissions.py | 4 ++-- backend/building/views.py | 6 +++--- readme/authorisation.md | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 1c78c71d..ee954f21 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -63,7 +63,7 @@ def has_permission(self, request, view): # OBJECT PERMISSIONS # ------------------ -class OwnerOfBuilding(BasePermission): +class ReadOnlyOwnerOfBuilding(BasePermission): """ Checks if the user owns the building """ @@ -73,7 +73,7 @@ def has_permission(self, request, view): return request.user.role.name.lower() == 'syndic' def has_object_permission(self, request, view, obj: Building): - if request.method in SAFE_METHODS + ['PATCH']: + if request.method in SAFE_METHODS: return request.user.id == obj.syndic_id return False diff --git a/backend/building/views.py b/backend/building/views.py index 1a7119a4..c81a8868 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -1,7 +1,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import OwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent, IsSyndic +from authorisation.permissions import ReadOnlyOwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent, IsSyndic from base.models import Building from base.serializers import BuildingSerializer from util.request_response_util import * @@ -33,7 +33,7 @@ def post(self, request): class BuildingIndividualView(APIView): permission_classes = [IsAuthenticated, - IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding + IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding ] def get(self, request, building_id): @@ -98,7 +98,7 @@ def get(self, request): class BuildingOwnerView(APIView): - permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerOfBuilding] def get(self, request, owner_id): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index 42e148f7..63d192c3 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -12,7 +12,7 @@ ### Object based permissions -- `OwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building +- `ReadOnlyOwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building - `OwnsAccount` (object): checks if the user tries to access his own user info - `CanEditUser` (object): checks if the user who tries to edit is in fact the user himself or someone with a higher rank - `CanEditRole` (object): checks if the user who tries to assign a role, doesn't set a role higher than his own role @@ -24,8 +24,8 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the ### Building urls - `building/ - [..., IsAdmin|IsSuperStudent]` -- `building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding]` -- `building/owner/id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding]` +- `building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding]` +- `building/owner/id - [..., IsAdmin | IsSuperStudent | ReadOnlyOwnerOfBuilding]` - `building/all - [...,IsAdmin | IsSuperStudent]` ### User urls From b69eed71207bfe196227e06d94d3b8bf79bbd5b3 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 19:53:54 +0100 Subject: [PATCH 0077/1000] authorisation building_comment --- backend/authorisation/permissions.py | 17 +++++++++++++++-- backend/building_comment/views.py | 16 +++++++++++++++- readme/authorisation.md | 5 +++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index ee954f21..bd199f99 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -63,12 +63,25 @@ def has_permission(self, request, view): # OBJECT PERMISSIONS # ------------------ -class ReadOnlyOwnerOfBuilding(BasePermission): +class OwnerOfBuilding(BasePermission): """ - Checks if the user owns the building + Check if the user owns the building """ message = "You can only access/edit the buildings that you own" + def has_permission(self, request, view): + return request.user.role.name.lower() == 'syndic' + + def has_object_permission(self, request, view, obj: Building): + return request.user.id == obj.syndic_id + + +class ReadOnlyOwnerOfBuilding(BasePermission): + """ + Checks if the user owns the building and only tries to read from it + """ + message = "You can only read the building that you own" + def has_permission(self, request, view): return request.user.role.name.lower() == 'syndic' diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index aae80b1b..c57a059a 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -1,5 +1,7 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerOfBuilding, ReadOnlyStudent from base.models import BuildingComment from base.serializers import BuildingCommentSerializer from util.request_response_util import * @@ -8,6 +10,7 @@ class DefaultBuildingComment(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding] def post(self, request): """ @@ -17,6 +20,8 @@ def post(self, request): building_comment_instance = BuildingComment() + self.check_object_permissions(request, building_comment_instance.building) + set_keys_of_instance(building_comment_instance, data, TRANSLATE) if r := try_full_clean_and_save(building_comment_instance): @@ -26,6 +31,7 @@ def post(self, request): class BuildingCommentIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent] def get(self, request, building_comment_id): """ @@ -33,6 +39,8 @@ def get(self, request, building_comment_id): """ building_comment_instance = BuildingComment.objects.filter(id=building_comment_id) + self.check_object_permissions(request, building_comment_instance.building) + if not building_comment_instance: return bad_request("BuildingComment") @@ -42,7 +50,9 @@ def delete(self, request, building_comment_id): """ Delete a BuildingComment with given id """ - building_comment_instance = BuildingComment.objectts.filter(id=building_comment_id) + building_comment_instance = BuildingComment.objects.filter(id=building_comment_id) + + self.check_object_permissions(request, building_comment_instance.building) if not building_comment_instance: return bad_request("BuildingComment") @@ -60,6 +70,8 @@ def patch(self, request, building_comment_id): return bad_request("BuildingComment") building_comment_instance = building_comment_instance[0] + self.check_object_permissions(request, building_comment_instance.building) + data = request_to_dict(request.data) set_keys_of_instance(building_comment_instance, data, TRANSLATE) @@ -71,6 +83,7 @@ def patch(self, request, building_comment_id): class BuildingCommentBuildingView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent] def get(self, request, building_id): """ @@ -86,6 +99,7 @@ def get(self, request, building_id): class BuildingCommentAllView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def get(self, request): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index 63d192c3..ed67dc68 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -28,6 +28,11 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `building/owner/id - [..., IsAdmin | IsSuperStudent | ReadOnlyOwnerOfBuilding]` - `building/all - [...,IsAdmin | IsSuperStudent]` +### BuildingComment urls +- `building/ - [..., IsAdmin | IsSuperStudent | OwnerOfBuildin]` +- `building/comment_id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent]` +- `building/building_id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From 37a1cf9fab33732684b9fc88fa3a1e84a78e73c0 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 20:10:12 +0100 Subject: [PATCH 0078/1000] authorisation building_on_tour --- backend/building_on_tour/views.py | 8 ++++++++ readme/authorisation.md | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/backend/building_on_tour/views.py b/backend/building_on_tour/views.py index c52de6da..a6e95fed 100644 --- a/backend/building_on_tour/views.py +++ b/backend/building_on_tour/views.py @@ -1,5 +1,7 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent from base.models import BuildingOnTour from base.serializers import BuildingTourSerializer from util.request_response_util import * @@ -8,6 +10,8 @@ class Default(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + def post(self, request): """ Create a new BuildingOnTour with data from post @@ -25,6 +29,8 @@ def post(self, request): class BuildingTourIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] + def get(self, request, building_tour_id): """ Get info about a BuildingOnTour with given id @@ -71,6 +77,8 @@ def delete(self, request, building_tour_id): class AllBuildingToursView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] + def get(self, request): """ Get all buildings on tours diff --git a/readme/authorisation.md b/readme/authorisation.md index ed67dc68..90ec0491 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -33,6 +33,11 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `building/comment_id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent]` - `building/building_id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent]` +### BuildingOnTour urls +- `building_on_tour/ - [...,IsAdmin | IsSuperStudent]` +- `building_on_tour/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` +- `building_on_tour/all - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From 782fc693300d6ced37600530da285e386658ea05 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 20:44:52 +0100 Subject: [PATCH 0079/1000] authorisation buildingurls --- backend/buildingurl/views.py | 31 +++++++++++++++++++++++++++++-- readme/authorisation.md | 7 +++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/backend/buildingurl/views.py b/backend/buildingurl/views.py index 516119dc..2b340a7c 100644 --- a/backend/buildingurl/views.py +++ b/backend/buildingurl/views.py @@ -1,6 +1,8 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.models import BuildingURL, Building +from authorisation.permissions import IsAdmin, OwnerOfBuilding, OwnsAccount +from base.models import BuildingURL, Building, User from base.serializers import BuildingUrlSerializer from util.request_response_util import * @@ -8,6 +10,7 @@ class BuildingUrlDefault(APIView): + permission_classes = [IsAuthenticated, IsAdmin | OwnerOfBuilding] def post(self, request): """ @@ -17,6 +20,8 @@ def post(self, request): building_url_instance = BuildingURL() + self.check_object_permissions(request, building_url_instance.building) + # Below line is necessary since we use RandomIDModel # Without this line, we would have a ValidationError because we do not have an id yet # save() calls the function from the parent class RandomIDModel @@ -37,6 +42,7 @@ def post(self, request): class BuildingUrlIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | OwnerOfBuilding] def get(self, request, building_url_id): """ @@ -46,6 +52,8 @@ def get(self, request, building_url_id): if not building_url_instance: return bad_request("BuildingUrl") + self.check_object_permissions(request, building_url_instance.building) + serializer = BuildingUrlSerializer(building_url_instance[0]) return get_success(serializer) @@ -56,8 +64,11 @@ def delete(self, request, building_url_id): building_url_instance = BuildingURL.objects.filter(id=building_url_id) if not building_url_instance: return bad_request("BuildingUrl") + building_url_instance = building_url_instance[0] - building_url_instance[0].delete() + self.check_object_permissions(request, building_url_instance.building) + + building_url_instance.delete() return delete_success() def patch(self, request, building_url_id): @@ -69,6 +80,8 @@ def patch(self, request, building_url_id): return bad_request("BuildingUrl") building_url_instance = building_url_instance[0] + self.check_object_permissions(request, building_url_instance.building) + data = request_to_dict(request.data) set_keys_of_instance(building_url_instance, data, TRANSLATE) @@ -85,10 +98,15 @@ class BuildingUrlSyndicView(APIView): /syndic/ """ + permission_classes = [IsAuthenticated, IsAdmin | OwnsAccount] + def get(self, request, syndic_id): """ Get all building urls of buildings where the user with given user id is syndic """ + id_holder = type("", (), {})() + id_holder.id = syndic_id + self.check_object_permissions(request, id_holder) # All building IDs where user is syndic building_ids = [building.id for building in Building.objects.filter(syndic=syndic_id)] @@ -103,21 +121,30 @@ class BuildingUrlBuildingView(APIView): building/ """ + permission_classes = [IsAuthenticated, IsAdmin | OwnerOfBuilding] + def get(self, request, building_id): """ Get all building urls of a given building """ + building_instance = Building.objects.filter(building_id=building_id)[0] + if not building_instance: + bad_request(building_instance) + self.check_object_permissions(request, building_instance) + building_url_instances = BuildingURL.objects.filter(building=building_id) serializer = BuildingUrlSerializer(building_url_instances, many=True) return get_success(serializer) class BuildingUrlAllView(APIView): + permission_classes = [IsAuthenticated, IsAdmin] def get(self, request): """ Get all building urls """ + building_url_instances = BuildingURL.objects.all() serializer = BuildingUrlSerializer(building_url_instances, many=True) return get_success(serializer) diff --git a/readme/authorisation.md b/readme/authorisation.md index 90ec0491..dbd61e45 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -38,6 +38,13 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `building_on_tour/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` - `building_on_tour/all - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` +### BuildingUrls urls +- `buildingurl/ - [..., IsAdmin | OwnerOfBuilding]` +- `buildingurl/id - [..., IsAdmin | OwnerOfBuilding]` +- `buildingurl/syndic/id - [..., IsAdmin | OwnsAccount]` +- `buildingurl/building/id - [..., IsAdmin | OwnerOfBuilding]` +- `buildingurl/all - [..., IsAdmin]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From 205e227a1ae38b8682b6ca1243f5c5582bb9c75c Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 20:54:58 +0100 Subject: [PATCH 0080/1000] authorisation garbage collection --- backend/garbage_collection/views.py | 15 ++++++++++++++- readme/authorisation.md | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/garbage_collection/views.py b/backend/garbage_collection/views.py index 1c26a2c1..1e768c3a 100644 --- a/backend/garbage_collection/views.py +++ b/backend/garbage_collection/views.py @@ -1,12 +1,16 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.models import GarbageCollection +from authorisation.permissions import IsSuperStudent, IsAdmin, ReadOnlyStudent, OwnerOfBuilding, ReadOnlyOwnerOfBuilding +from base.models import GarbageCollection, Building from base.serializers import GarbageCollectionSerializer from util.request_response_util import * TRANSLATE = {"building": "building_id"} + class DefaultGarbageCollection(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def post(self, request): """ @@ -26,6 +30,7 @@ def post(self, request): class GarbageCollectionIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] def get(self, request, garbage_collection_id): """ @@ -72,16 +77,24 @@ class GarbageCollectionIndividualBuildingView(APIView): /building/ """ + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding] + def get(self, request, building_id): """ Get info about all garbage collections of a building with given id """ + building_instance = Building.objects.filter(building_id=building_id) + if not (building_instance): + bad_request(building_instance) + self.check_object_permissions(request, building_instance[0]) + garbage_collection_instances = GarbageCollection.objects.filter(building=building_id) serializer = GarbageCollectionSerializer(garbage_collection_instances, many=True) return get_success(serializer) class GarbageCollectionAllView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def get(self, request): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index dbd61e45..4bc10ce7 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -29,22 +29,32 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `building/all - [...,IsAdmin | IsSuperStudent]` ### BuildingComment urls + - `building/ - [..., IsAdmin | IsSuperStudent | OwnerOfBuildin]` - `building/comment_id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent]` - `building/building_id - [..., IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent]` ### BuildingOnTour urls + - `building_on_tour/ - [...,IsAdmin | IsSuperStudent]` - `building_on_tour/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` - `building_on_tour/all - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` ### BuildingUrls urls + - `buildingurl/ - [..., IsAdmin | OwnerOfBuilding]` - `buildingurl/id - [..., IsAdmin | OwnerOfBuilding]` - `buildingurl/syndic/id - [..., IsAdmin | OwnsAccount]` - `buildingurl/building/id - [..., IsAdmin | OwnerOfBuilding]` - `buildingurl/all - [..., IsAdmin]` +### Garbage Collection + +- `garbage_collection/ - [..., IsAdmin | IsSuperStudent]` +- `garbage_collection/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` +- `garbage_collection/building/id - [IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding]` +- `garbage_collection/all - [..., IsAdmin | IsSuperStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From 76f434c64d89648a453c7821bbf7819beaa289e1 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 21:28:42 +0100 Subject: [PATCH 0081/1000] authorisation manual --- backend/manual/views.py | 11 +++++++++++ readme/authorisation.md | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/backend/manual/views.py b/backend/manual/views.py index 0255009b..9834e440 100644 --- a/backend/manual/views.py +++ b/backend/manual/views.py @@ -1,5 +1,7 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, IsSuperStudent, IsSyndic, OwnerOfBuilding, ReadOnlyStudent from base.models import Manual, Building from base.serializers import ManualSerializer from util.request_response_util import * @@ -8,6 +10,8 @@ class Default(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsSyndic] + def post(self, request): """ Create a new manual with data from post @@ -24,6 +28,9 @@ def post(self, request): class ManualView(APIView): + # TODO: Change IsSyndic to the owner of the manual (once that is added to the Manual model) + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | IsSyndic] + def get(self, request, manual_id): """ Get info about a manual with given id @@ -63,6 +70,8 @@ def patch(self, request, manual_id): class ManualBuildingView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding] + def get(self, request, building_id): """ Get all manuals of a building with given id @@ -76,6 +85,8 @@ def get(self, request, building_id): class ManualsView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + def get(self, request): """ Get all manuals diff --git a/readme/authorisation.md b/readme/authorisation.md index 4bc10ce7..de4f09a0 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -55,6 +55,14 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `garbage_collection/building/id - [IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding]` - `garbage_collection/all - [..., IsAdmin | IsSuperStudent]` +### Manual + +- `manual/ - [..., IsAdmin | IsSuperStudent | IsSyndic]` +- `manual/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | IsSyndic]` + - **TODO** Change IsSyndic to IsOwnerOfManual once that is added to the model +- `manual/building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding]` +- `manual/all/ - [..., IsAdmin | IsSuperStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From fb22e0de32e871c13955826ae01ee70d5482e57a Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 21:37:40 +0100 Subject: [PATCH 0082/1000] authorisation picture_building --- backend/picture_building/views.py | 32 ++++++++++++++++++++++++++++--- readme/authorisation.md | 7 +++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 02abe429..e11cca85 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -1,6 +1,8 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.models import PictureBuilding +from authorisation.permissions import IsAdmin, IsSuperStudent, IsStudent, ReadOnlyOwnerOfBuilding +from base.models import PictureBuilding, Building from base.serializers import PictureBuildingSerializer from util.request_response_util import * @@ -8,6 +10,8 @@ class Default(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] + def post(self, request): """ Create a new PictureBuilding @@ -24,6 +28,8 @@ def post(self, request): class PictureBuildingIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding] + def get(self, request, picture_building_id): """ Get PictureBuilding with given id @@ -32,7 +38,11 @@ def get(self, request, picture_building_id): if len(picture_building_instance) != 1: return bad_request("PictureBuilding") - serializer = PictureBuildingSerializer(picture_building_instance[0]) + picture_building_instance = picture_building_instance[0] + + self.check_object_permissions(request, picture_building_instance.building) + + serializer = PictureBuildingSerializer(picture_building_instance) return get_success(serializer) def patch(self, request, picture_building_id): @@ -44,6 +54,7 @@ def patch(self, request, picture_building_id): return bad_request("PictureBuilding") picture_building_instance = picture_building_instance[0] + self.check_object_permissions(request, picture_building_instance) data = request_to_dict(request.data) @@ -61,21 +72,36 @@ def delete(self, request, picture_building_id): picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) if len(picture_building_instance) != 1: return bad_request("PictureBuilding") - picture_building_instance[0].delete() + picture_building_instance = picture_building_instance[0] + + self.check_object_permissions(request, picture_building_instance) + + picture_building_instance.delete() return delete_success() class PicturesOfBuildingView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding] + def get(self, request, building_id): """ Get all pictures of a building with given id """ + building_instance = Building.objects.filter(building_id=building_id) + if not building_instance: + return bad_request(building_instance) + building_instance = building_instance[0] + + self.check_object_permissions(request, building_instance) + picture_building_instances = PictureBuilding.objects.filter(building_id=building_id) serializer = PictureBuildingSerializer(picture_building_instances, many=True) return get_success(serializer) class AllPictureBuildingsView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + def get(self, request): """ Get all pictureBuilding diff --git a/readme/authorisation.md b/readme/authorisation.md index de4f09a0..8235573c 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -63,6 +63,13 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `manual/building/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding]` - `manual/all/ - [..., IsAdmin | IsSuperStudent]` +### PictureBuilding + +- `picture_building/ - [..., IsAdmin | IsSuperStudent | IsStudent]` +- `picture_building/id - [..., IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding]` +- `picture_building/building/id - [..., IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding]` +- `picture_building/all - [..., IsAdmin | IsSuperStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From 5b4efb72161435c7d7c78dfb5e0b538259184fdb Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 21:42:54 +0100 Subject: [PATCH 0083/1000] authorisation region --- backend/authorisation/permissions.py | 8 ++++++++ backend/region/views.py | 9 +++++---- readme/authorisation.md | 5 +++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index bd199f99..317e758e 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -59,6 +59,14 @@ def has_permission(self, request, view): return request.user.role.name.lower() == 'syndic' +# ------------------ +# ACTION PERMISSIONS +# ------------------ +class ReadOnly(BasePermission): + def has_permission(self, request, view): + return request in SAFE_METHODS + + # ------------------ # OBJECT PERMISSIONS # ------------------ diff --git a/backend/region/views.py b/backend/region/views.py index ed3a03a2..f29273fc 100644 --- a/backend/region/views.py +++ b/backend/region/views.py @@ -1,13 +1,14 @@ -from rest_framework import permissions +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, ReadOnly, IsSuperStudent, IsStudent from base.models import Region from base.serializers import RegionSerializer from util.request_response_util import * class Default(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin] def post(self, request): """ @@ -27,7 +28,7 @@ def post(self, request): class RegionIndividualView(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin | ReadOnly] def get(self, request, region_id): """ @@ -75,7 +76,7 @@ def delete(self, request, region_id): class AllRegionsView(APIView): - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] def get(self, request): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index 8235573c..8517b8f9 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -70,6 +70,11 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `picture_building/building/id - [..., IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding]` - `picture_building/all - [..., IsAdmin | IsSuperStudent]` +### Region +- `region/ - [..., IsAdmin]` +- `region/id - [..., IsAdmin | ReadOnly]` +- `region/all - [..., IsAdmin | IsSuperStudent | IsStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From b2aca7c768d7c8c344d07a8da872f3e005d85cc9 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 21:46:47 +0100 Subject: [PATCH 0084/1000] authorisation roles --- backend/role/views.py | 6 +++++- readme/authorisation.md | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/role/views.py b/backend/role/views.py index 70f3715e..60c52b99 100644 --- a/backend/role/views.py +++ b/backend/role/views.py @@ -1,12 +1,14 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, IsSuperStudent from base.models import Role from base.serializers import RoleSerializer from util.request_response_util import * - class DefaultRoleView(APIView): + permission_classes = [IsAuthenticated, IsAdmin] def post(self, request): """ @@ -25,6 +27,7 @@ def post(self, request): class RoleIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def get(self, request, role_id): """ @@ -72,6 +75,7 @@ def patch(self, request, role_id): class AllRolesView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] def get(self, request): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index 8517b8f9..4411059e 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -71,10 +71,17 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `picture_building/all - [..., IsAdmin | IsSuperStudent]` ### Region + - `region/ - [..., IsAdmin]` - `region/id - [..., IsAdmin | ReadOnly]` - `region/all - [..., IsAdmin | IsSuperStudent | IsStudent]` +### Role + +- `role/ - [..., IsAdmin]` +- `role/id - [..., IsAdmin | IsSuperStudent]` +- `role/all - [..., IsAdmin | IsSuperStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From e8f758b73eef7162f6a60e5dbd3452ebef5b28a2 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 22:01:18 +0100 Subject: [PATCH 0085/1000] authorisation student_at_building_on_tour --- backend/authorisation/permissions.py | 14 +++++++++- backend/buildingurl/views.py | 4 +-- backend/student_at_building_on_tour/views.py | 27 ++++++++++++++++++-- backend/users/views.py | 4 +-- readme/authorisation.md | 14 +++++++++- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 317e758e..77d7518e 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -99,7 +99,7 @@ def has_object_permission(self, request, view, obj: Building): return False -class OwnsAccount(BasePermission): +class OwnerAccount(BasePermission): """ Checks if the user is owns the user account """ @@ -109,6 +109,18 @@ def has_object_permission(self, request, view, obj: User): return request.user.id == obj.id +class ReadOnlyOwnerAccount(BasePermission): + """ + Checks if the user is owns the user account + """ + message = "You can only access/edit your own account" + + def has_object_permission(self, request, view, obj: User): + if request.method in SAFE_METHODS: + return request.user.id == obj.id + return False + + class CanEditUser(BasePermission): """ Checks if the user has the right permissions to edit diff --git a/backend/buildingurl/views.py b/backend/buildingurl/views.py index 2b340a7c..40c70c1a 100644 --- a/backend/buildingurl/views.py +++ b/backend/buildingurl/views.py @@ -1,7 +1,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import IsAdmin, OwnerOfBuilding, OwnsAccount +from authorisation.permissions import IsAdmin, OwnerOfBuilding, OwnerAccount from base.models import BuildingURL, Building, User from base.serializers import BuildingUrlSerializer from util.request_response_util import * @@ -98,7 +98,7 @@ class BuildingUrlSyndicView(APIView): /syndic/ """ - permission_classes = [IsAuthenticated, IsAdmin | OwnsAccount] + permission_classes = [IsAuthenticated, IsAdmin | OwnerAccount] def get(self, request, syndic_id): """ diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index 6e2b94b8..876cf3b2 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -1,5 +1,7 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, ReadOnlyOwnerAccount from base.models import StudentAtBuildingOnTour from base.serializers import StudBuildTourSerializer from util.request_response_util import * @@ -8,6 +10,8 @@ class Default(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + def post(self, request): """ Create a new StudentAtBuildingOnTour @@ -24,16 +28,24 @@ def post(self, request): class BuildingTourPerStudentView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerAccount] + def get(self, request, student_id): """ Get all StudentAtBuildingOnTour for a student with given id """ + id_holder = type("", (), {})() + id_holder.id = student_id + self.check_object_permissions(request, id_holder) + student_at_building_on_tour_instances = StudentAtBuildingOnTour.objects.filter(student_id=student_id) serializer = StudBuildTourSerializer(student_at_building_on_tour_instances, many=True) return get_success(serializer) class StudentAtBuildingOnTourIndividualView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerAccount] + def get(self, request, student_at_building_on_tour_id): """ Get an individual StudentAtBuildingOnTour with given id @@ -42,8 +54,11 @@ def get(self, request, student_at_building_on_tour_id): if len(stud_tour_building_instance) != 1: return bad_request("StudentAtBuildingOnTour") + stud_tour_building_instance = stud_tour_building_instance[0] + + self.check_object_permissions(request, stud_tour_building_instance.student) - serializer = StudBuildTourSerializer(stud_tour_building_instance[0]) + serializer = StudBuildTourSerializer(stud_tour_building_instance) return get_success(serializer) def patch(self, request, student_at_building_on_tour_id): @@ -57,6 +72,8 @@ def patch(self, request, student_at_building_on_tour_id): stud_tour_building_instance = stud_tour_building_instances[0] + self.check_object_permissions(request, stud_tour_building_instance.student) + data = request_to_dict(request.data) set_keys_of_instance(stud_tour_building_instance, data, TRANSLATE) @@ -74,11 +91,17 @@ def delete(self, request, student_at_building_on_tour_id): stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) if len(stud_tour_building_instances) != 1: return bad_request("StudentAtBuildingOnTour") - stud_tour_building_instances[0].delete() + stud_tour_building_instance = stud_tour_building_instances[0] + + self.check_object_permissions(request, stud_tour_building_instance.student) + + stud_tour_building_instance.delete() return delete_success() class AllView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + def get(self, request): """ Get all StudentAtBuildingOnTours diff --git a/backend/users/views.py b/backend/users/views.py index 7feffc6b..95d95b5d 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import IsAdmin, IsSuperStudent, OwnsAccount, CanEditUser, CanEditRole +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, CanEditUser, CanEditRole from base.models import User from base.serializers import UserSerializer from util.request_response_util import * @@ -60,7 +60,7 @@ def post(self, request): class UserIndividualView(APIView): - permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser, CanEditRole] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerAccount, CanEditUser, CanEditRole] def get(self, request, user_id): """ diff --git a/readme/authorisation.md b/readme/authorisation.md index 4411059e..765802ff 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -13,10 +13,15 @@ ### Object based permissions - `ReadOnlyOwnerOfBuilding` (global + object): checks if the user is a syndic and if he owns the building -- `OwnsAccount` (object): checks if the user tries to access his own user info +- `OwnerAccount` (object): checks if the user tries to access his own user info or info that belongs to him/her +- `ReadOnlyOwnerAccount` (object): checks if the user tries to read his own user info or info that belongs to him/her - `CanEditUser` (object): checks if the user who tries to edit is in fact the user himself or someone with a higher rank - `CanEditRole` (object): checks if the user who tries to assign a role, doesn't set a role higher than his own role +### Action based permissions + +- `ReadOnly` (global): checks if the method is a safe method (`GET`, `HEAD`, `OPTIONS`) + ## Protected endpoints For all these views, `IsAuthenticated` is required. Therefor we only mention the interesting permissions here. @@ -82,6 +87,13 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `role/id - [..., IsAdmin | IsSuperStudent]` - `role/all - [..., IsAdmin | IsSuperStudent]` +### Student at building on tour + +- `student_at_building_on_tour/ - [..., IsAdmin | IsSuperStudent]` +- `student_at_building_on_tour/id - [..., IsAdmin | IsSuperStudent | ReadOnlyOwnerAccount]` +- `student_at_building_on_tour/student/id - [..., IsAdmin | IsSuperStudent | OwnerAccount]` +- `student_at_building_on_tour/all - [..., IsAdmin | IsSuperStudent]` + ### User urls - `user/ - [..., IsAdmin | IsSuperStudent]` From 01c2e928063705b7aaa59cb0de3fce4bf1075543 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 15 Mar 2023 22:02:51 +0100 Subject: [PATCH 0086/1000] change order endpoints in documentation --- readme/authorisation.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/readme/authorisation.md b/readme/authorisation.md index 765802ff..9191bd8b 100644 --- a/readme/authorisation.md +++ b/readme/authorisation.md @@ -94,14 +94,15 @@ For all these views, `IsAuthenticated` is required. Therefor we only mention the - `student_at_building_on_tour/student/id - [..., IsAdmin | IsSuperStudent | OwnerAccount]` - `student_at_building_on_tour/all - [..., IsAdmin | IsSuperStudent]` -### User urls - -- `user/ - [..., IsAdmin | IsSuperStudent]` -- `user/id - [..., IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser, CanEditRole]` -- `user/all - [..., IsAdmin | IsSuperStudent]` - ### Tour urls - `tour/ - [..., IsAdmin | IsSuperStudent]` - `tour/id - [..., IsAdmin | IsSuperStudent | ReadOnlyStudent]` -- `tour/all - [..., IsAdmin | IsSuperStudent]` \ No newline at end of file +- `tour/all - [..., IsAdmin | IsSuperStudent]` + + +### User urls + +- `user/ - [..., IsAdmin | IsSuperStudent]` +- `user/id - [..., IsAuthenticated, IsAdmin | IsSuperStudent | OwnsAccount, CanEditUser, CanEditRole]` +- `user/all - [..., IsAdmin | IsSuperStudent]` \ No newline at end of file From 415b40c511ff6bf598094eb5b5835374c40a2fa6 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 17 Mar 2023 10:40:09 +0100 Subject: [PATCH 0087/1000] Removed alert response at signupu --- frontend/lib/signup.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/lib/signup.tsx b/frontend/lib/signup.tsx index 62214705..b036cc1b 100644 --- a/frontend/lib/signup.tsx +++ b/frontend/lib/signup.tsx @@ -25,7 +25,6 @@ const signup = async (firstname: string, lastname: string, email: string, passwo headers: {"Content-Type": "application/json"}, body: JSON.stringify(signup_data), }); - alert(await response.text()); if (response.status == 201) { alert("Successfully created account"); await router.push("/login"); From d1be9bc471d509a3be8dc662dd37dcd8e7d297a7 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 17 Mar 2023 19:29:20 +0100 Subject: [PATCH 0088/1000] Added model EmailWhitelist & EmailTemplate + removed BuildingURL model and app --- backend/base/admin.py | 3 +- ...ilwhitelist_building_public_id_and_more.py | 41 +++++ backend/base/models.py | 32 ++-- backend/base/serializers.py | 7 - backend/buildingurl/__init__.py | 0 backend/buildingurl/apps.py | 6 - backend/buildingurl/tests.py | 3 - backend/buildingurl/urls.py | 17 -- backend/buildingurl/views.py | 145 ------------------ backend/config/urls.py | 2 - 10 files changed, 66 insertions(+), 190 deletions(-) create mode 100644 backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py delete mode 100644 backend/buildingurl/__init__.py delete mode 100644 backend/buildingurl/apps.py delete mode 100644 backend/buildingurl/tests.py delete mode 100644 backend/buildingurl/urls.py delete mode 100644 backend/buildingurl/views.py diff --git a/backend/base/admin.py b/backend/base/admin.py index ede5fca8..f9b0301e 100644 --- a/backend/base/admin.py +++ b/backend/base/admin.py @@ -4,7 +4,6 @@ admin.site.register(User) admin.site.register(Region) admin.site.register(Building) -admin.site.register(BuildingURL) admin.site.register(GarbageCollection) admin.site.register(Tour) admin.site.register(BuildingOnTour) @@ -13,3 +12,5 @@ admin.site.register(Manual) admin.site.register(BuildingComment) admin.site.register(Role) +admin.site.register(EmailWhitelist) +admin.site.register(EmailTemplate) diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py new file mode 100644 index 00000000..ae409c91 --- /dev/null +++ b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.7 on 2023-03-17 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EmailTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('template', models.TextField()), + ], + ), + migrations.CreateModel( + name='EmailWhitelist', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(error_messages={'unique': 'This email is already on the whitelist.'}, max_length=254, unique=True, verbose_name='email address')), + ('verification_code', models.CharField(error_messages={'unique': 'This verification code already exists.'}, max_length=128, unique=True)), + ], + ), + migrations.AddField( + model_name='building', + name='public_id', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.DeleteModel( + name='BuildingURL', + ), + migrations.AddConstraint( + model_name='emailtemplate', + constraint=models.UniqueConstraint(models.F('name'), name='unique_template_name', violation_error_message='The name for this template already exists.'), + ), + ] diff --git a/backend/base/models.py b/backend/base/models.py index 33eb854a..3aacbb71 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -86,6 +86,14 @@ def __str__(self): return f"{self.email} ({self.role})" +class EmailWhitelist(models.Model): + email = models.EmailField(_('email address'), unique=True, + error_messages={'unique': "This email is already on the whitelist."}) + # The verification code, preferably hashed + verification_code = models.CharField(max_length=128, unique=True, + error_messages={'unique': "This verification code already exists."}) + + class Building(models.Model): city = models.CharField(max_length=40) postal_code = models.CharField(max_length=10) @@ -96,6 +104,7 @@ class Building(models.Model): syndic = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) region = models.ForeignKey(Region, on_delete=models.SET_NULL, blank=True, null=True) name = models.CharField(max_length=100, blank=True, null=True) + public_id = models.CharField(max_length=32, blank=True, null=True) ''' Only a syndic can own a building, not a student. @@ -128,15 +137,6 @@ def __str__(self): return f"{self.street} {self.house_number}, {self.city} {self.postal_code}" -class BuildingURL(RandomIDModel): - first_name_resident = models.CharField(max_length=40) - last_name_resident = models.CharField(max_length=40) - building = models.ForeignKey(Building, on_delete=models.CASCADE) - - def __str__(self): - return f"{self.first_name_resident} {self.last_name_resident} : {self.id}" - - class BuildingComment(models.Model): comment = models.TextField() date = models.DateTimeField() @@ -377,3 +377,17 @@ class Meta: violation_error_message='The building already has a manual with the same version number' ), ] + + +class EmailTemplate(models.Model): + name = models.CharField(max_length=40) + template = models.TextField() + + class Meta: + constraints = [ + UniqueConstraint( + 'name', + name='unique_template_name', + violation_error_message='The name for this template already exists.' + ), + ] diff --git a/backend/base/serializers.py b/backend/base/serializers.py index f15751a3..3d81babc 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -45,13 +45,6 @@ class Meta: fields = ["id", "building_on_tour", "date", "student"] -class BuildingUrlSerializer(serializers.ModelSerializer): - class Meta: - model = BuildingURL - fields = ["id", "first_name_resident", "last_name_resident", "building"] - read_only_fields = ["id"] - - class GarbageCollectionSerializer(serializers.ModelSerializer): class Meta: model = GarbageCollection diff --git a/backend/buildingurl/__init__.py b/backend/buildingurl/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/buildingurl/apps.py b/backend/buildingurl/apps.py deleted file mode 100644 index cee2b369..00000000 --- a/backend/buildingurl/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class BuildingurlConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'buildingurl' diff --git a/backend/buildingurl/tests.py b/backend/buildingurl/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/backend/buildingurl/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/buildingurl/urls.py b/backend/buildingurl/urls.py deleted file mode 100644 index dbb9a43a..00000000 --- a/backend/buildingurl/urls.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.urls import path - -from .views import ( - BuildingUrlAllView, - BuildingUrlIndividualView, - BuildingUrlSyndicView, - BuildingUrlBuildingView, - BuildingUrlDefault -) - -urlpatterns = [ - path('all/', BuildingUrlAllView.as_view()), - path('/', BuildingUrlIndividualView.as_view()), - path('syndic//', BuildingUrlSyndicView.as_view()), - path('building//', BuildingUrlBuildingView.as_view()), - path('', BuildingUrlDefault.as_view()) -] diff --git a/backend/buildingurl/views.py b/backend/buildingurl/views.py deleted file mode 100644 index b612d287..00000000 --- a/backend/buildingurl/views.py +++ /dev/null @@ -1,145 +0,0 @@ -from rest_framework.views import APIView - -from base.models import BuildingURL, Building -from base.serializers import BuildingUrlSerializer -from util.request_response_util import * -from drf_spectacular.utils import extend_schema - -TRANSLATE = {"building": "building_id"} - - -class BuildingUrlDefault(APIView): - serializer_class = BuildingUrlSerializer - - @extend_schema( - responses={201: BuildingUrlSerializer, - 400: None} - ) - def post(self, request): - """ - Create a new building url - """ - data = request_to_dict(request.data) - - building_url_instance = BuildingURL() - - # Below line is necessary since we use RandomIDModel - # Without this line, we would have a ValidationError because we do not have an id yet - # save() calls the function from the parent class RandomIDModel - # The try is needed because the save will fail because there is no building etc. given - # (It's a dirty fix, but it works) - try: - building_url_instance.save() - except IntegrityError: - pass - - set_keys_of_instance(building_url_instance, data, TRANSLATE) - - if r := try_full_clean_and_save(building_url_instance): - return r - - serializer = BuildingUrlSerializer(building_url_instance) - return post_success(serializer) - - -class BuildingUrlIndividualView(APIView): - serializer_class = BuildingUrlSerializer - - @extend_schema( - responses={200: BuildingUrlSerializer, - 400: None} - ) - def get(self, request, building_url_id): - """ - Get info about a buildingurl with given id - """ - building_url_instance = BuildingURL.objects.filter(id=building_url_id) - if not building_url_instance: - return bad_request("BuildingUrl") - - serializer = BuildingUrlSerializer(building_url_instance[0]) - return get_success(serializer) - - @extend_schema( - responses={204: None, - 400: None} - ) - def delete(self, request, building_url_id): - """ - Delete buildingurl with given id - """ - building_url_instance = BuildingURL.objects.filter(id=building_url_id) - if not building_url_instance: - return bad_request("BuildingUrl") - - building_url_instance[0].delete() - return delete_success() - - @extend_schema( - responses={200: BuildingUrlSerializer, - 400: None} - ) - def patch(self, request, building_url_id): - """ - Edit info about buildingurl with given id - """ - building_url_instance = BuildingURL.objects.filter(id=building_url_id) - if not building_url_instance: - return bad_request("BuildingUrl") - - building_url_instance = building_url_instance[0] - data = request_to_dict(request.data) - - set_keys_of_instance(building_url_instance, data, TRANSLATE) - - if r := try_full_clean_and_save(building_url_instance): - return r - - serializer = BuildingUrlSerializer(building_url_instance) - return patch_success(serializer) - - -class BuildingUrlSyndicView(APIView): - """ - /syndic/ - """ - serializer_class = BuildingUrlSerializer - - def get(self, request, syndic_id): - """ - Get all building urls of buildings where the user with given user id is syndic - """ - - # All building IDs where user is syndic - building_ids = [building.id for building in Building.objects.filter(syndic=syndic_id)] - - building_urls_instances = BuildingURL.objects.filter(building__in=building_ids) - serializer = BuildingUrlSerializer(building_urls_instances, many=True) - return get_success(serializer) - - -class BuildingUrlBuildingView(APIView): - """ - building/ - """ - serializer_class = BuildingUrlSerializer - - def get(self, request, building_id): - """ - Get all building urls of a given building - """ - building_url_instances = BuildingURL.objects.filter(building=building_id) - serializer = BuildingUrlSerializer(building_url_instances, many=True) - return get_success(serializer) - - -class BuildingUrlAllView(APIView): - serializer_class = BuildingUrlSerializer - - def get(self, request): - """ - Get all building urls - """ - building_url_instances = BuildingURL.objects.all() - serializer = BuildingUrlSerializer(building_url_instances, many=True) - return get_success(serializer) diff --git a/backend/config/urls.py b/backend/config/urls.py index 731457a2..0dda3c64 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -23,7 +23,6 @@ from building import urls as building_urls from building_comment import urls as building_comment_urls from building_on_tour import urls as building_on_tour_urls -from buildingurl import urls as building_url_urls from garbage_collection import urls as garbage_collection_urls from manual import urls as manual_urls from picture_building import urls as picture_building_urls @@ -44,7 +43,6 @@ path('building/', include(building_urls)), path('building_comment/', include(building_comment_urls)), path('region/', include(region_urls)), - path('buildingurl/', include(building_url_urls)), path('garbage_collection/', include(garbage_collection_urls)), path('building_on_tour/', include(building_on_tour_urls)), path('user/', include(user_urls)), From 1b7c9427a0c1bc67b6c8ca1e4edab477ff21e1bf Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 17 Mar 2023 20:03:16 +0100 Subject: [PATCH 0089/1000] Added bus number field in building model --- ..._emailtemplate_emailwhitelist_building_bus_and_more.py} | 7 ++++++- backend/base/models.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) rename backend/base/migrations/{0002_emailtemplate_emailwhitelist_building_public_id_and_more.py => 0002_emailtemplate_emailwhitelist_building_bus_and_more.py} (87%) diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py similarity index 87% rename from backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py rename to backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py index ae409c91..adc0e835 100644 --- a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_public_id_and_more.py +++ b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-03-17 18:27 +# Generated by Django 4.1.7 on 2023-03-17 19:02 from django.db import migrations, models @@ -26,6 +26,11 @@ class Migration(migrations.Migration): ('verification_code', models.CharField(error_messages={'unique': 'This verification code already exists.'}, max_length=128, unique=True)), ], ), + migrations.AddField( + model_name='building', + name='bus', + field=models.CharField(blank=True, max_length=2, null=True), + ), migrations.AddField( model_name='building', name='public_id', diff --git a/backend/base/models.py b/backend/base/models.py index 3aacbb71..59dbf047 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -99,6 +99,7 @@ class Building(models.Model): postal_code = models.CharField(max_length=10) street = models.CharField(max_length=60) house_number = models.CharField(max_length=10) + bus = models.CharField(max_length=2, blank=True, null=True) client_number = models.CharField(max_length=40, blank=True, null=True) duration = models.TimeField(default='00:00') syndic = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) From 2dc5a7bcce212a731080379b04b4325d41135a4a Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Sat, 18 Mar 2023 01:15:15 +0100 Subject: [PATCH 0090/1000] Changed house number of building to positive integer field --- ...te_emailwhitelist_building_bus_and_more.py | 9 ++++-- backend/base/models.py | 8 +++-- backend/datadump.json | 32 +++++++++---------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py index adc0e835..3fcc732d 100644 --- a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py +++ b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-03-17 19:02 +# Generated by Django 4.1.7 on 2023-03-18 00:13 from django.db import migrations, models @@ -29,13 +29,18 @@ class Migration(migrations.Migration): migrations.AddField( model_name='building', name='bus', - field=models.CharField(blank=True, max_length=2, null=True), + field=models.CharField(blank=True, max_length=10, null=True), ), migrations.AddField( model_name='building', name='public_id', field=models.CharField(blank=True, max_length=32, null=True), ), + migrations.AlterField( + model_name='building', + name='house_number', + field=models.PositiveIntegerField(), + ), migrations.DeleteModel( name='BuildingURL', ), diff --git a/backend/base/models.py b/backend/base/models.py index 59dbf047..7e2b6090 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -98,8 +98,8 @@ class Building(models.Model): city = models.CharField(max_length=40) postal_code = models.CharField(max_length=10) street = models.CharField(max_length=60) - house_number = models.CharField(max_length=10) - bus = models.CharField(max_length=2, blank=True, null=True) + house_number = models.PositiveIntegerField() + bus = models.CharField(max_length=10, blank=True, null=True) client_number = models.CharField(max_length=40, blank=True, null=True) duration = models.TimeField(default='00:00') syndic = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) @@ -117,8 +117,10 @@ def clean(self): # If this is not checked, `self.syndic` will cause an internal server error 500 _check_for_present_keys(self, {"syndic_id"}) - user = self.syndic + if self.house_number == 0: + raise ValidationError("The house number of the building must be positive and not zero.") + user = self.syndic if user.role.name.lower() != 'syndic': raise ValidationError("Only a user with role \"syndic\" can own a building.") diff --git a/backend/datadump.json b/backend/datadump.json index fea70dee..96752f5e 100644 --- a/backend/datadump.json +++ b/backend/datadump.json @@ -539,7 +539,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Grote Markt", - "house_number": "1", + "house_number": 1, "client_number": "48943513", "duration": "00:30:00", "syndic": [ @@ -556,7 +556,7 @@ "city": "Gent", "postal_code": "9000", "street": "Veldstraat", - "house_number": "1", + "house_number": 1, "client_number": null, "duration": "00:45:00", "syndic": [ @@ -573,7 +573,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Universiteitsplein", - "house_number": "1", + "house_number": 1, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -590,7 +590,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Groenenborgerlaan", - "house_number": "171", + "house_number": 171, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -607,7 +607,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Middelheimlaan", - "house_number": "1", + "house_number": 1, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -624,7 +624,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Prinsstraat", - "house_number": "13", + "house_number": 13, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -641,7 +641,7 @@ "city": "Gent", "postal_code": "9000", "street": "Krijgslaan", - "house_number": "281", + "house_number": 281, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -658,7 +658,7 @@ "city": "Gent", "postal_code": "9000", "street": "Karel Lodewijk Ledeganckstraat", - "house_number": "35", + "house_number": 35, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -675,7 +675,7 @@ "city": "Gent", "postal_code": "9000", "street": "Tweekerkenstraat", - "house_number": "2", + "house_number": 2, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -692,7 +692,7 @@ "city": "Gent", "postal_code": "9000", "street": "Sint-Pietersnieuwstraat", - "house_number": "33", + "house_number": 33, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -709,7 +709,7 @@ "city": "Gent", "postal_code": "9000", "street": "Veldstraat", - "house_number": "2", + "house_number": 2, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -726,7 +726,7 @@ "city": "Gent", "postal_code": "9000", "street": "Veldstraat", - "house_number": "3", + "house_number": 3, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -743,7 +743,7 @@ "city": "Gent", "postal_code": "9000", "street": "Veldstraat", - "house_number": "4", + "house_number": 4, "client_number": null, "duration": "01:00:00", "syndic": [ @@ -760,7 +760,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Grote Markt", - "house_number": "2", + "house_number": 2, "client_number": null, "duration": "00:00:00", "syndic": [ @@ -777,7 +777,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Grote Markt", - "house_number": "3", + "house_number": 3, "client_number": null, "duration": "00:00:00", "syndic": [ @@ -794,7 +794,7 @@ "city": "Antwerpen", "postal_code": "2000", "street": "Grote Markt", - "house_number": "4", + "house_number": 4, "client_number": null, "duration": "00:00:00", "syndic": [ From 2d176be83e616c21a22f13225c4d0bd3a862fca0 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Sat, 18 Mar 2023 15:32:47 +0100 Subject: [PATCH 0091/1000] move backend from port 2002 to '/api/' location. --- backend/config/urls.py | 2 +- nginx/nginx.conf | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/config/urls.py b/backend/config/urls.py index 731457a2..e83e757a 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -37,7 +37,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path('docs/', SpectacularAPIView.as_view(), name='schema'), - path('docs/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + path('docs/ui/', SpectacularSwaggerView.as_view(url='/api/docs'), name='swagger-ui'), path('authentication/', include(authentication_urls)), path('manual/', include(manual_urls)), path('picture_building/', include(picture_building_urls)), diff --git a/nginx/nginx.conf b/nginx/nginx.conf index cdfbc130..2ebd9b04 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -27,30 +27,34 @@ http { server { listen 80 default_server; listen [::]:80; + # for development purposes + location ~ ^/api/(?.*)$ { + proxy_pass http://docker-backend/$rest; + } location / { proxy_pass http://docker-frontend; } + # on server: + # return 301 https://$host$request_uri; } server { listen 443 default_server; # todo add ssl listen [::]:443; # todo add ssl + + location ~ ^/api/(?.*)$ { + proxy_pass http://docker-backend/$rest; +# proxy_redirect http:// https://; + } + location / { proxy_pass http://docker-frontend; -# proxy_redirect http:// https://; } + # ssl settings for when deployed + # ssl_certificate '/etc/letsencrypt/live/sel2-4.ugent.be/fullchain.pem'; # ssl_certificate_key '/etc/letsencrypt/live/sel2-4.ugent.be/privkey.pem'; # include /etc/letsencrypt/options-ssl-nginx.conf; } - - server { - listen 2002; - server_name localhost; - location / { - proxy_pass http://docker-backend; - proxy_redirect off; - } - } } \ No newline at end of file From 3a9ba8ba62732f31cc295d39fd5f9055d706f880 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 16:06:56 +0100 Subject: [PATCH 0092/1000] #18 removed linter, as discussed in meeting --- .github/workflows/formatting.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 237fbb2e..9ecff0ed 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -32,22 +32,4 @@ jobs: prettier_options: --write **/*.{js,tsx} commit_message: "Auto formatted code" only_changed: true - github_token: ${{ secrets.GITHUB_TOKEN }} - lint: - needs: [format] - name: Lint Code Base - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - - - name: Lint Code Base - uses: github/super-linter@v4 - env: - VALIDATE_ALL_CODEBASE: false - DEFAULT_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 3507e25dffccf29efd48666232c0c583a5968644 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 15:19:05 +0000 Subject: [PATCH 0093/1000] Auto formatted code --- backend/authentication/apps.py | 4 +- backend/authentication/urls.py | 50 +- backend/authentication/views.py | 58 +- backend/base/apps.py | 4 +- backend/base/migrations/0001_initial.py | 532 ++++++++++++++----- backend/base/models.py | 235 ++++---- backend/base/serializers.py | 26 +- backend/building/apps.py | 4 +- backend/building/urls.py | 10 +- backend/building/views.py | 25 +- backend/building_comment/apps.py | 4 +- backend/building_comment/urls.py | 10 +- backend/building_comment/views.py | 41 +- backend/building_on_tour/apps.py | 4 +- backend/building_on_tour/urls.py | 12 +- backend/building_on_tour/views.py | 20 +- backend/buildingurl/apps.py | 4 +- backend/buildingurl/urls.py | 12 +- backend/buildingurl/views.py | 26 +- backend/config/asgi.py | 2 +- backend/config/middleware.py | 4 +- backend/config/secrets.sample.py | 6 +- backend/config/settings.py | 182 ++++--- backend/config/urls.py | 38 +- backend/config/wsgi.py | 2 +- backend/garbage_collection/apps.py | 4 +- backend/garbage_collection/urls.py | 10 +- backend/garbage_collection/views.py | 46 +- backend/manage.py | 4 +- backend/manual/apps.py | 4 +- backend/manual/urls.py | 15 +- backend/manual/views.py | 25 +- backend/picture_building/apps.py | 4 +- backend/picture_building/urls.py | 10 +- backend/picture_building/views.py | 36 +- backend/region/apps.py | 4 +- backend/region/urls.py | 12 +- backend/region/views.py | 21 +- backend/role/apps.py | 4 +- backend/role/urls.py | 12 +- backend/role/views.py | 20 +- backend/student_at_building_on_tour/apps.py | 4 +- backend/student_at_building_on_tour/urls.py | 13 +- backend/student_at_building_on_tour/views.py | 44 +- backend/tour/apps.py | 4 +- backend/tour/urls.py | 12 +- backend/tour/views.py | 21 +- backend/users/managers.py | 4 +- backend/users/tests.py | 8 +- backend/users/urls.py | 12 +- backend/users/views.py | 21 +- backend/util/request_response_util.py | 10 +- frontend/components/header/BaseHeader.tsx | 29 +- frontend/context/AuthProvider.tsx | 69 ++- frontend/lib/login.tsx | 51 +- frontend/lib/reset.tsx | 45 +- frontend/lib/signup.tsx | 68 +-- frontend/next.config.js | 12 +- frontend/pages/_app.tsx | 19 +- frontend/pages/_document.tsx | 20 +- frontend/pages/api/axios.tsx | 60 +-- frontend/pages/index.tsx | 3 +- frontend/pages/login.tsx | 126 +++-- frontend/pages/reset-password.tsx | 97 ++-- frontend/pages/signup.tsx | 195 ++++--- frontend/pages/welcome.tsx | 126 ++--- frontend/types.d.tsx | 22 +- 67 files changed, 1471 insertions(+), 1170 deletions(-) diff --git a/backend/authentication/apps.py b/backend/authentication/apps.py index 8bab8df0..c65f1d28 100644 --- a/backend/authentication/apps.py +++ b/backend/authentication/apps.py @@ -2,5 +2,5 @@ class AuthenticationConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'authentication' + default_auto_field = "django.db.models.BigAutoField" + name = "authentication" diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 3bc1d5d8..5bd4f60b 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -1,26 +1,42 @@ from dj_rest_auth.registration.views import VerifyEmailView -from dj_rest_auth.views import PasswordResetView, PasswordResetConfirmView, PasswordChangeView +from dj_rest_auth.views import ( + PasswordResetView, + PasswordResetConfirmView, + PasswordChangeView, +) from django.urls import path, include from rest_framework_simplejwt.views import TokenVerifyView -from authentication.views import LoginViewWithHiddenTokens, RefreshViewHiddenTokens, LogoutViewWithBlacklisting +from authentication.views import ( + LoginViewWithHiddenTokens, + RefreshViewHiddenTokens, + LogoutViewWithBlacklisting, +) urlpatterns = [ # URLs that do not require a session or valid token - - path('signup/', include('dj_rest_auth.registration.urls')), - path('password/reset/', PasswordResetView.as_view()), - path('password/reset/confirm///', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), - path('login/', LoginViewWithHiddenTokens.as_view(), name='rest_login'), - path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), - path('token/refresh/', RefreshViewHiddenTokens.as_view(), name='token_refresh'), - + path("signup/", include("dj_rest_auth.registration.urls")), + path("password/reset/", PasswordResetView.as_view()), + path( + "password/reset/confirm///", + PasswordResetConfirmView.as_view(), + name="password_reset_confirm", + ), + path("login/", LoginViewWithHiddenTokens.as_view(), name="rest_login"), + path("token/verify/", TokenVerifyView.as_view(), name="token_verify"), + path("token/refresh/", RefreshViewHiddenTokens.as_view(), name="token_refresh"), # URLs that require a user to be logged in with a valid session / token. - - path('logout/', LogoutViewWithBlacklisting.as_view(), name='rest_logout'), - path('password/change/', PasswordChangeView.as_view(), name='rest_password_change'), - path('verify-email/', VerifyEmailView.as_view(), name="rest_verify_email"), - path('account-confirm-email/', VerifyEmailView.as_view(), name='account_confirm_email_sent', ), - path('account-confirm-email//', VerifyEmailView.as_view(), name='account_confirm_email', ) - + path("logout/", LogoutViewWithBlacklisting.as_view(), name="rest_logout"), + path("password/change/", PasswordChangeView.as_view(), name="rest_password_change"), + path("verify-email/", VerifyEmailView.as_view(), name="rest_verify_email"), + path( + "account-confirm-email/", + VerifyEmailView.as_view(), + name="account_confirm_email_sent", + ), + path( + "account-confirm-email//", + VerifyEmailView.as_view(), + name="account_confirm_email", + ), ] diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 18c7b45c..222507ac 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -1,5 +1,9 @@ -from dj_rest_auth.jwt_auth import unset_jwt_cookies, CookieTokenRefreshSerializer, set_jwt_access_cookie, \ - set_jwt_refresh_cookie +from dj_rest_auth.jwt_auth import ( + unset_jwt_cookies, + CookieTokenRefreshSerializer, + set_jwt_access_cookie, + set_jwt_refresh_cookie, +) from dj_rest_auth.views import LogoutView, LoginView from django.utils.translation import gettext_lazy as _ from rest_framework import status @@ -15,18 +19,14 @@ class LogoutViewWithBlacklisting(LogoutView): serializer_class = CookieTokenRefreshSerializer - @extend_schema( - responses={200: None, - 401: None, - 500: None} - ) + @extend_schema(responses={200: None, 401: None, 500: None}) def logout(self, request): response = Response( - {'detail': _('Successfully logged out.')}, + {"detail": _("Successfully logged out.")}, status=status.HTTP_200_OK, ) - cookie_name = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE', None) + cookie_name = getattr(settings, "JWT_AUTH_REFRESH_COOKIE", None) unset_jwt_cookies(response) @@ -35,19 +35,23 @@ def logout(self, request): token = RefreshToken(request.COOKIES.get(cookie_name)) token.blacklist() except KeyError: - response.data = {'detail': _( - 'Refresh token was not included in request cookies.')} + response.data = { + "detail": _("Refresh token was not included in request cookies.") + } response.status_code = status.HTTP_401_UNAUTHORIZED except (TokenError, AttributeError, TypeError) as error: - if hasattr(error, 'args'): - if 'Token is blacklisted' in error.args or 'Token is invalid or expired' in error.args: - response.data = {'detail': _(error.args[0])} + if hasattr(error, "args"): + if ( + "Token is blacklisted" in error.args + or "Token is invalid or expired" in error.args + ): + response.data = {"detail": _(error.args[0])} response.status_code = status.HTTP_401_UNAUTHORIZED else: - response.data = {'detail': _('An error has occurred.')} + response.data = {"detail": _("An error has occurred.")} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR else: - response.data = {'detail': _('An error has occurred.')} + response.data = {"detail": _("An error has occurred.")} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return response @@ -56,16 +60,16 @@ class RefreshViewHiddenTokens(TokenRefreshView): serializer_class = CookieTokenRefreshSerializer def finalize_response(self, request, response, *args, **kwargs): - if response.status_code == 200 and 'access' in response.data: + if response.status_code == 200 and "access" in response.data: set_jwt_access_cookie(response, response.data["access"]) - response.data['access-token-refresh'] = _('success') + response.data["access-token-refresh"] = _("success") # we don't want this info to be in the body for security reasons (HTTPOnly!) - del response.data['access'] - if response.status_code == 200 and 'refresh' in response.data: - set_jwt_refresh_cookie(response, response.data['refresh']) - response.data['refresh-token-rotation'] = _('success') + del response.data["access"] + if response.status_code == 200 and "refresh" in response.data: + set_jwt_refresh_cookie(response, response.data["refresh"]) + response.data["refresh-token-rotation"] = _("success") # we don't want this info to be in the body for security reasons (HTTPOnly!) - del response.data['refresh'] + del response.data["refresh"] return super().finalize_response(request, response, *args, **kwargs) @@ -73,9 +77,9 @@ class LoginViewWithHiddenTokens(LoginView): # serializer_class = CookieTokenRefreshSerializer def finalize_response(self, request, response, *args, **kwargs): - if response.status_code == 200 and 'access_token' in response.data: - response.data['access_token'] = _('set successfully') - if response.status_code == 200 and 'refresh_token' in response.data: - response.data['refresh_token'] = _('set successfully') + if response.status_code == 200 and "access_token" in response.data: + response.data["access_token"] = _("set successfully") + if response.status_code == 200 and "refresh_token" in response.data: + response.data["refresh_token"] = _("set successfully") return super().finalize_response(request, response, *args, **kwargs) diff --git a/backend/base/apps.py b/backend/base/apps.py index 05011e82..bca3fb07 100644 --- a/backend/base/apps.py +++ b/backend/base/apps.py @@ -2,5 +2,5 @@ class BaseConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'base' + default_auto_field = "django.db.models.BigAutoField" + name = "base" diff --git a/backend/base/migrations/0001_initial.py b/backend/base/migrations/0001_initial.py index b3396fec..b7edefe0 100644 --- a/backend/base/migrations/0001_initial.py +++ b/backend/base/migrations/0001_initial.py @@ -8,234 +8,514 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='User', + 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')), - ('email', models.EmailField(error_messages={'unique': 'A user already exists with this email.'}, max_length=254, unique=True, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=True)), - ('first_name', models.CharField(max_length=40)), - ('last_name', models.CharField(max_length=40)), - ('phone_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, region='BE')), + ( + "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", + ), + ), + ( + "email", + models.EmailField( + error_messages={ + "unique": "A user already exists with this email." + }, + max_length=254, + unique=True, + verbose_name="email address", + ), + ), + ("is_staff", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("first_name", models.CharField(max_length=40)), + ("last_name", models.CharField(max_length=40)), + ( + "phone_number", + phonenumber_field.modelfields.PhoneNumberField( + max_length=128, region="BE" + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Building', + name="Building", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('city', models.CharField(max_length=40)), - ('postal_code', models.CharField(max_length=10)), - ('street', models.CharField(max_length=60)), - ('house_number', models.CharField(max_length=10)), - ('client_number', models.CharField(blank=True, max_length=40, null=True)), - ('duration', models.TimeField(default='00:00')), - ('name', models.CharField(blank=True, max_length=100, null=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("city", models.CharField(max_length=40)), + ("postal_code", models.CharField(max_length=10)), + ("street", models.CharField(max_length=60)), + ("house_number", models.CharField(max_length=10)), + ( + "client_number", + models.CharField(blank=True, max_length=40, null=True), + ), + ("duration", models.TimeField(default="00:00")), + ("name", models.CharField(blank=True, max_length=100, null=True)), ], ), migrations.CreateModel( - name='BuildingComment', + name="BuildingComment", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('comment', models.TextField()), - ('date', models.DateTimeField()), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("comment", models.TextField()), + ("date", models.DateTimeField()), ], ), migrations.CreateModel( - name='BuildingOnTour', + name="BuildingOnTour", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('index', models.PositiveIntegerField()), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("index", models.PositiveIntegerField()), ], ), migrations.CreateModel( - name='BuildingURL', + name="BuildingURL", fields=[ - ('id', models.BigIntegerField(primary_key=True, serialize=False)), - ('first_name_resident', models.CharField(max_length=40)), - ('last_name_resident', models.CharField(max_length=40)), + ("id", models.BigIntegerField(primary_key=True, serialize=False)), + ("first_name_resident", models.CharField(max_length=40)), + ("last_name_resident", models.CharField(max_length=40)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='GarbageCollection', + name="GarbageCollection", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField()), - ('garbage_type', models.CharField(choices=[('GFT', 'GFT'), ('GLS', 'Glas'), ('GRF', 'Grof vuil'), ('KER', 'Kerstbomen'), ('PAP', 'Papier'), ('PMD', 'PMD'), ('RES', 'Restafval')], max_length=3)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField()), + ( + "garbage_type", + models.CharField( + choices=[ + ("GFT", "GFT"), + ("GLS", "Glas"), + ("GRF", "Grof vuil"), + ("KER", "Kerstbomen"), + ("PAP", "Papier"), + ("PMD", "PMD"), + ("RES", "Restafval"), + ], + max_length=3, + ), + ), ], ), migrations.CreateModel( - name='Manual', + name="Manual", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version_number', models.PositiveIntegerField(default=0)), - ('file', models.FileField(blank=True, null=True, upload_to='building_manuals/')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("version_number", models.PositiveIntegerField(default=0)), + ( + "file", + models.FileField( + blank=True, null=True, upload_to="building_manuals/" + ), + ), ], ), migrations.CreateModel( - name='PictureBuilding', + name="PictureBuilding", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('picture', models.ImageField(blank=True, null=True, upload_to='building_pictures/')), - ('description', models.TextField(blank=True, null=True)), - ('timestamp', models.DateTimeField()), - ('type', models.CharField(choices=[('AA', 'Aankomst'), ('BI', 'Binnen'), ('VE', 'Vertrek'), ('OP', 'Opmerking')], max_length=2)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "picture", + models.ImageField( + blank=True, null=True, upload_to="building_pictures/" + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("timestamp", models.DateTimeField()), + ( + "type", + models.CharField( + choices=[ + ("AA", "Aankomst"), + ("BI", "Binnen"), + ("VE", "Vertrek"), + ("OP", "Opmerking"), + ], + max_length=2, + ), + ), ], ), migrations.CreateModel( - name='Region', + name="Region", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('region', models.CharField(error_messages={'unique': 'Deze regio bestaat al.'}, max_length=40, unique=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "region", + models.CharField( + error_messages={"unique": "Deze regio bestaat al."}, + max_length=40, + unique=True, + ), + ), ], ), migrations.CreateModel( - name='Role', + name="Role", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=20)), - ('rank', models.PositiveIntegerField()), - ('description', models.TextField(blank=True, null=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=20)), + ("rank", models.PositiveIntegerField()), + ("description", models.TextField(blank=True, null=True)), ], ), migrations.CreateModel( - name='Tour', + name="Tour", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40)), - ('modified_at', models.DateTimeField(blank=True, null=True)), - ('region', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.region')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=40)), + ("modified_at", models.DateTimeField(blank=True, null=True)), + ( + "region", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="base.region", + ), + ), ], ), migrations.CreateModel( - name='StudentAtBuildingOnTour', + name="StudentAtBuildingOnTour", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField()), - ('building_on_tour', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.buildingontour')), - ('student', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField()), + ( + "building_on_tour", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="base.buildingontour", + ), + ), + ( + "student", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddConstraint( - model_name='role', - constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), name='role_unique', violation_error_message='This role name already exists.'), + model_name="role", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + name="role_unique", + violation_error_message="This role name already exists.", + ), ), migrations.AddField( - model_name='picturebuilding', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.building'), + model_name="picturebuilding", + name="building", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.building" + ), ), migrations.AddField( - model_name='manual', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.building'), + model_name="manual", + name="building", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.building" + ), ), migrations.AddField( - model_name='garbagecollection', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.building'), + model_name="garbagecollection", + name="building", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.building" + ), ), migrations.AddField( - model_name='buildingurl', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.building'), + model_name="buildingurl", + name="building", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.building" + ), ), migrations.AddField( - model_name='buildingontour', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.building'), + model_name="buildingontour", + name="building", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.building" + ), ), migrations.AddField( - model_name='buildingontour', - name='tour', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.tour'), + model_name="buildingontour", + name="tour", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.tour" + ), ), migrations.AddField( - model_name='buildingcomment', - name='building', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.building'), + model_name="buildingcomment", + name="building", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="base.building" + ), ), migrations.AddField( - model_name='building', - name='region', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.region'), + model_name="building", + name="region", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="base.region", + ), ), migrations.AddField( - model_name='building', - name='syndic', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + model_name="building", + name="syndic", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='user', - name='groups', - field=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'), + model_name="user", + name="groups", + field=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", + ), ), migrations.AddField( - model_name='user', - name='region', - field=models.ManyToManyField(to='base.region'), + model_name="user", + name="region", + field=models.ManyToManyField(to="base.region"), ), migrations.AddField( - model_name='user', - name='role', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.role'), + model_name="user", + name="role", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="base.role", + ), ), migrations.AddField( - model_name='user', - name='user_permissions', - field=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'), + model_name="user", + name="user_permissions", + field=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", + ), ), migrations.AddConstraint( - model_name='tour', - constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('name'), models.F('region'), name='unique_tour', violation_error_message='There is already a tour with the same name in the region.'), + model_name="tour", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + models.F("region"), + name="unique_tour", + violation_error_message="There is already a tour with the same name in the region.", + ), ), migrations.AddConstraint( - model_name='studentatbuildingontour', - constraint=models.UniqueConstraint(models.F('building_on_tour'), models.F('date'), models.F('student'), name='unique_student_at_building_on_tour', violation_error_message='The student is already assigned to this tour on this date.'), + model_name="studentatbuildingontour", + constraint=models.UniqueConstraint( + models.F("building_on_tour"), + models.F("date"), + models.F("student"), + name="unique_student_at_building_on_tour", + violation_error_message="The student is already assigned to this tour on this date.", + ), ), migrations.AddConstraint( - model_name='picturebuilding', - constraint=models.UniqueConstraint(models.F('building'), django.db.models.functions.text.Lower('picture'), django.db.models.functions.text.Lower('description'), models.F('timestamp'), name='unique_picture_building', violation_error_message='The building already has the upload.'), + model_name="picturebuilding", + constraint=models.UniqueConstraint( + models.F("building"), + django.db.models.functions.text.Lower("picture"), + django.db.models.functions.text.Lower("description"), + models.F("timestamp"), + name="unique_picture_building", + violation_error_message="The building already has the upload.", + ), ), migrations.AddConstraint( - model_name='manual', - constraint=models.UniqueConstraint(models.F('building_id'), models.F('version_number'), name='unique_manual', violation_error_message='The building already has a manual with the same version number'), + model_name="manual", + constraint=models.UniqueConstraint( + models.F("building_id"), + models.F("version_number"), + name="unique_manual", + violation_error_message="The building already has a manual with the same version number", + ), ), migrations.AddConstraint( - model_name='garbagecollection', - constraint=models.UniqueConstraint(models.F('building'), django.db.models.functions.text.Lower('garbage_type'), models.F('date'), name='garbage_collection_unique', violation_error_message='This type of garbage is already being collected on the same day for this building.'), + model_name="garbagecollection", + constraint=models.UniqueConstraint( + models.F("building"), + django.db.models.functions.text.Lower("garbage_type"), + models.F("date"), + name="garbage_collection_unique", + violation_error_message="This type of garbage is already being collected on the same day for this building.", + ), ), migrations.AddConstraint( - model_name='buildingontour', - constraint=models.UniqueConstraint(models.F('index'), models.F('tour'), name='unique_index_on_tour', violation_error_message='The tour has already a building on this index.'), + model_name="buildingontour", + constraint=models.UniqueConstraint( + models.F("index"), + models.F("tour"), + name="unique_index_on_tour", + violation_error_message="The tour has already a building on this index.", + ), ), migrations.AddConstraint( - model_name='buildingontour', - constraint=models.UniqueConstraint(models.F('building'), models.F('tour'), name='unique_building_on_tour', violation_error_message='This building is already on this tour.'), + model_name="buildingontour", + constraint=models.UniqueConstraint( + models.F("building"), + models.F("tour"), + name="unique_building_on_tour", + violation_error_message="This building is already on this tour.", + ), ), migrations.AddConstraint( - model_name='buildingcomment', - constraint=models.UniqueConstraint(models.F('building'), django.db.models.functions.text.Lower('comment'), models.F('date'), name='building_comment_unique', violation_error_message='This comment already exists, and was posted at the exact same time.'), + model_name="buildingcomment", + constraint=models.UniqueConstraint( + models.F("building"), + django.db.models.functions.text.Lower("comment"), + models.F("date"), + name="building_comment_unique", + violation_error_message="This comment already exists, and was posted at the exact same time.", + ), ), migrations.AddConstraint( - model_name='building', - constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('city'), django.db.models.functions.text.Lower('street'), django.db.models.functions.text.Lower('postal_code'), django.db.models.functions.text.Lower('house_number'), name='address_unique', violation_error_message='A building with this address already exists.'), + model_name="building", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("city"), + django.db.models.functions.text.Lower("street"), + django.db.models.functions.text.Lower("postal_code"), + django.db.models.functions.text.Lower("house_number"), + name="address_unique", + violation_error_message="A building with this address already exists.", + ), ), ] diff --git a/backend/base/models.py b/backend/base/models.py index 33eb854a..ad9f004c 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -14,17 +14,21 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2 ** 31 - 1 +MAX_INT = 2**31 - 1 def _check_for_present_keys(instance, keys_iterable): for key in keys_iterable: if not vars(instance)[key]: - raise ValidationError(f"Tried to access {key}, but it was not found in object") + raise ValidationError( + f"Tried to access {key}, but it was not found in object" + ) class Region(models.Model): - region = models.CharField(max_length=40, unique=True, error_messages={'unique': "Deze regio bestaat al."}) + region = models.CharField( + max_length=40, unique=True, error_messages={"unique": "Deze regio bestaat al."} + ) def __str__(self): return self.region @@ -36,6 +40,7 @@ def __str__(self): # if created: # Token.objects.create(user=instance) + class Role(models.Model): name = models.CharField(max_length=20) rank = models.PositiveIntegerField() @@ -47,16 +52,18 @@ def __str__(self): def clean(self): super().clean() if Role.objects.count() != 0 and self.rank != MAX_INT: - highest_rank = Role.objects.order_by('-rank').first().rank + highest_rank = Role.objects.order_by("-rank").first().rank if self.rank > highest_rank + 1: - raise ValidationError(f"The maximum rank allowed is {highest_rank + 1}.") + raise ValidationError( + f"The maximum rank allowed is {highest_rank + 1}." + ) class Meta: constraints = [ UniqueConstraint( - Lower('name'), - name='role_unique', - violation_error_message='This role name already exists.' + Lower("name"), + name="role_unique", + violation_error_message="This role name already exists.", ), ] @@ -64,17 +71,20 @@ class Meta: class User(AbstractBaseUser, PermissionsMixin): username = None # extra fields for authentication - email = models.EmailField(_('email address'), unique=True, - error_messages={'unique': "A user already exists with this email."}) + email = models.EmailField( + _("email address"), + unique=True, + error_messages={"unique": "A user already exists with this email."}, + ) is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=True) - USERNAME_FIELD = 'email' # there is a username field and a password field - REQUIRED_FIELDS = ['first_name', 'last_name', 'phone_number', 'role'] + USERNAME_FIELD = "email" # there is a username field and a password field + REQUIRED_FIELDS = ["first_name", "last_name", "phone_number", "role"] first_name = models.CharField(max_length=40) last_name = models.CharField(max_length=40) - phone_number = PhoneNumberField(region='BE') + phone_number = PhoneNumberField(region="BE") region = models.ManyToManyField(Region) # This is the new role model @@ -92,14 +102,14 @@ class Building(models.Model): street = models.CharField(max_length=60) house_number = models.CharField(max_length=10) client_number = models.CharField(max_length=40, blank=True, null=True) - duration = models.TimeField(default='00:00') + duration = models.TimeField(default="00:00") syndic = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) region = models.ForeignKey(Region, on_delete=models.SET_NULL, blank=True, null=True) name = models.CharField(max_length=100, blank=True, null=True) - ''' + """ Only a syndic can own a building, not a student. - ''' + """ def clean(self): super().clean() @@ -109,18 +119,18 @@ def clean(self): user = self.syndic - if user.role.name.lower() != 'syndic': - raise ValidationError("Only a user with role \"syndic\" can own a building.") + if user.role.name.lower() != "syndic": + raise ValidationError('Only a user with role "syndic" can own a building.') class Meta: constraints = [ UniqueConstraint( - Lower('city'), - Lower('street'), - Lower('postal_code'), - Lower('house_number'), - name='address_unique', - violation_error_message='A building with this address already exists.' + Lower("city"), + Lower("street"), + Lower("postal_code"), + Lower("house_number"), + name="address_unique", + violation_error_message="A building with this address already exists.", ), ] @@ -148,11 +158,11 @@ def __str__(self): class Meta: constraints = [ UniqueConstraint( - 'building', - Lower('comment'), - 'date', - name='building_comment_unique', - violation_error_message='This comment already exists, and was posted at the exact same time.' + "building", + Lower("comment"), + "date", + name="building_comment_unique", + violation_error_message="This comment already exists, and was posted at the exact same time.", ), ] @@ -161,25 +171,23 @@ class GarbageCollection(models.Model): building = models.ForeignKey(Building, on_delete=models.CASCADE) date = models.DateField() - GFT = 'GFT' - GLAS = 'GLS' - GROF_VUIL = 'GRF' - KERSTBOMEN = 'KER' - PAPIER = 'PAP' - PMD = 'PMD' - RESTAFVAL = 'RES' + GFT = "GFT" + GLAS = "GLS" + GROF_VUIL = "GRF" + KERSTBOMEN = "KER" + PAPIER = "PAP" + PMD = "PMD" + RESTAFVAL = "RES" GARBAGE = [ - (GFT, 'GFT'), - (GLAS, 'Glas'), - (GROF_VUIL, 'Grof vuil'), - (KERSTBOMEN, 'Kerstbomen'), - (PAPIER, 'Papier'), - (PMD, 'PMD'), - (RESTAFVAL, 'Restafval') + (GFT, "GFT"), + (GLAS, "Glas"), + (GROF_VUIL, "Grof vuil"), + (KERSTBOMEN, "Kerstbomen"), + (PAPIER, "Papier"), + (PMD, "PMD"), + (RESTAFVAL, "Restafval"), ] - garbage_type = models.CharField( - max_length=3, - choices=GARBAGE) + garbage_type = models.CharField(max_length=3, choices=GARBAGE) def __str__(self): return f"{self.garbage_type} on {self.date} at {self.building}" @@ -187,12 +195,12 @@ def __str__(self): class Meta: constraints = [ UniqueConstraint( - 'building', - Lower('garbage_type'), - 'date', - name='garbage_collection_unique', - violation_error_message='This type of garbage is already being collected on the same day for this ' - 'building.' + "building", + Lower("garbage_type"), + "date", + name="garbage_collection_unique", + violation_error_message="This type of garbage is already being collected on the same day for this " + "building.", ), ] @@ -216,10 +224,10 @@ def __str__(self): class Meta: constraints = [ UniqueConstraint( - Lower('name'), - 'region', - name='unique_tour', - violation_error_message='There is already a tour with the same name in the region.' + Lower("name"), + "region", + name="unique_tour", + violation_error_message="There is already a tour with the same name in the region.", ), ] @@ -229,9 +237,9 @@ class BuildingOnTour(models.Model): building = models.ForeignKey(Building, on_delete=models.CASCADE) index = models.PositiveIntegerField() - ''' + """ The region of a tour and of a building needs to be the same. - ''' + """ def clean(self): super().clean() @@ -241,12 +249,16 @@ def clean(self): tour_region = self.tour.region building_region = self.building.region if tour_region != building_region: - raise ValidationError(f"The regions for tour ({tour_region}) en building ({building_region}) " - f"are different.") + raise ValidationError( + f"The regions for tour ({tour_region}) en building ({building_region}) " + f"are different." + ) nr_of_buildings = BuildingOnTour.objects.filter(tour=self.tour).count() if self.index > nr_of_buildings: - raise ValidationError(f"The maximum allowed index for this building is {nr_of_buildings}") + raise ValidationError( + f"The maximum allowed index for this building is {nr_of_buildings}" + ) def __str__(self): return f"{self.building} on tour {self.tour}, index: {self.index}" @@ -254,49 +266,56 @@ def __str__(self): class Meta: constraints = [ UniqueConstraint( - 'index', - 'tour', - name='unique_index_on_tour', - violation_error_message='The tour has already a building on this index.' + "index", + "tour", + name="unique_index_on_tour", + violation_error_message="The tour has already a building on this index.", ), UniqueConstraint( - 'building', - 'tour', - name='unique_building_on_tour', - violation_error_message='This building is already on this tour.' - ) + "building", + "tour", + name="unique_building_on_tour", + violation_error_message="This building is already on this tour.", + ), ] class StudentAtBuildingOnTour(models.Model): - building_on_tour = models.ForeignKey(BuildingOnTour, on_delete=models.SET_NULL, null=True) + building_on_tour = models.ForeignKey( + BuildingOnTour, on_delete=models.SET_NULL, null=True + ) date = models.DateField() student = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - ''' + """ A syndic can't do tours, so we need to check that a student assigned to the building on the tour is not a syndic. Also, the student that does the tour needs to have selected the region where the building is located. - ''' + """ def clean(self): super().clean() _check_for_present_keys(self, {"student_id", "building_on_tour_id", "date"}) user = self.student - if user.role.name.lower() == 'syndic': + if user.role.name.lower() == "syndic": raise ValidationError("A syndic can't do tours") building_on_tour_region = self.building_on_tour.tour.region - if not self.student.region.all().filter(region=building_on_tour_region).exists(): + if ( + not self.student.region.all() + .filter(region=building_on_tour_region) + .exists() + ): raise ValidationError( - f"Student ({user.email}) doesn't do tours in this region ({building_on_tour_region}).") + f"Student ({user.email}) doesn't do tours in this region ({building_on_tour_region})." + ) class Meta: constraints = [ UniqueConstraint( - 'building_on_tour', - 'date', - 'student', - name='unique_student_at_building_on_tour', - violation_error_message='The student is already assigned to this tour on this date.' + "building_on_tour", + "date", + "student", + name="unique_student_at_building_on_tour", + violation_error_message="The student is already assigned to this tour on this date.", ), ] @@ -306,39 +325,39 @@ def __str__(self): class PictureBuilding(models.Model): building = models.ForeignKey(Building, on_delete=models.CASCADE) - picture = models.ImageField(upload_to='building_pictures/', blank=True, null=True) + picture = models.ImageField(upload_to="building_pictures/", blank=True, null=True) description = models.TextField(blank=True, null=True) timestamp = models.DateTimeField() - AANKOMST = 'AA' - BINNEN = 'BI' - VERTREK = 'VE' - OPMERKING = 'OP' + AANKOMST = "AA" + BINNEN = "BI" + VERTREK = "VE" + OPMERKING = "OP" TYPE = [ - (AANKOMST, 'Aankomst'), - (BINNEN, 'Binnen'), - (VERTREK, 'Vertrek'), - (OPMERKING, 'Opmerking') + (AANKOMST, "Aankomst"), + (BINNEN, "Binnen"), + (VERTREK, "Vertrek"), + (OPMERKING, "Opmerking"), ] - type = models.CharField( - max_length=2, - choices=TYPE) + type = models.CharField(max_length=2, choices=TYPE) def clean(self): super().clean() - _check_for_present_keys(self, {"building_id", "picture", "description", "timestamp"}) + _check_for_present_keys( + self, {"building_id", "picture", "description", "timestamp"} + ) class Meta: constraints = [ UniqueConstraint( - 'building', - Lower('picture'), - Lower('description'), - 'timestamp', - name='unique_picture_building', - violation_error_message='The building already has the upload.' + "building", + Lower("picture"), + Lower("description"), + "timestamp", + name="unique_picture_building", + violation_error_message="The building already has the upload.", ), ] @@ -349,7 +368,7 @@ def __str__(self): class Manual(models.Model): building = models.ForeignKey(Building, on_delete=models.CASCADE) version_number = models.PositiveIntegerField(default=0) - file = models.FileField(upload_to='building_manuals/', blank=True, null=True) + file = models.FileField(upload_to="building_manuals/", blank=True, null=True) def __str__(self): return f"Manual: {str(self.file).split('/')[-1]} (version {self.version_number}) for {self.building}" @@ -365,15 +384,19 @@ def clean(self): version_numbers.add(-1) max_version_number = max(version_numbers) - if self.version_number == 0 or self.version_number > max_version_number + 1 or self.version_number in version_numbers: + if ( + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers + ): self.version_number = max_version_number + 1 class Meta: constraints = [ UniqueConstraint( - 'building_id', - 'version_number', - name='unique_manual', - violation_error_message='The building already has a manual with the same version number' + "building_id", + "version_number", + name="unique_manual", + violation_error_message="The building already has a manual with the same version number", ), ] diff --git a/backend/base/serializers.py b/backend/base/serializers.py index f15751a3..7e28d3dc 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -6,8 +6,16 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ["id", "is_active", "email", "first_name", "last_name", - "phone_number", "region", "role"] + fields = [ + "id", + "is_active", + "email", + "first_name", + "last_name", + "phone_number", + "region", + "role", + ] read_only_fields = ["id", "email"] @@ -21,8 +29,18 @@ class Meta: class BuildingSerializer(serializers.ModelSerializer): class Meta: model = Building - fields = ["id", "city", "postal_code", "street", "house_number", "client_number", - "duration", "syndic", "region", "name"] + fields = [ + "id", + "city", + "postal_code", + "street", + "house_number", + "client_number", + "duration", + "syndic", + "region", + "name", + ] read_only_fields = ["id"] diff --git a/backend/building/apps.py b/backend/building/apps.py index f795a650..d05d0888 100644 --- a/backend/building/apps.py +++ b/backend/building/apps.py @@ -2,5 +2,5 @@ class BuildingConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'building' + default_auto_field = "django.db.models.BigAutoField" + name = "building" diff --git a/backend/building/urls.py b/backend/building/urls.py index ccc6383e..3b86ced9 100644 --- a/backend/building/urls.py +++ b/backend/building/urls.py @@ -4,12 +4,12 @@ BuildingIndividualView, BuildingOwnerView, AllBuildingsView, - DefaultBuilding + DefaultBuilding, ) urlpatterns = [ - path('/', BuildingIndividualView.as_view()), - path('all/', AllBuildingsView.as_view()), - path('owner//', BuildingOwnerView.as_view()), - path('', DefaultBuilding.as_view()), + path("/", BuildingIndividualView.as_view()), + path("all/", AllBuildingsView.as_view()), + path("owner//", BuildingOwnerView.as_view()), + path("", DefaultBuilding.as_view()), ] diff --git a/backend/building/views.py b/backend/building/views.py index 2682e2af..f656c333 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -15,10 +15,7 @@ class DefaultBuilding(APIView): permission_classes = [permissions.IsAuthenticated] serializer_class = BuildingSerializer - @extend_schema( - responses={201: BuildingSerializer, - 400: None} - ) + @extend_schema(responses={201: BuildingSerializer, 400: None}) def post(self, request): """ Create a new building @@ -40,10 +37,7 @@ class BuildingIndividualView(APIView): permission_classes = [permissions.IsAuthenticated] serializer_class = BuildingSerializer - @extend_schema( - responses={200: BuildingSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingSerializer, 400: None}) def get(self, request, building_id): """ Get info about building with given id @@ -57,10 +51,7 @@ def get(self, request, building_id): serializer = BuildingSerializer(building_instance) return get_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, building_id): """ Delete building with given id @@ -73,10 +64,7 @@ def delete(self, request, building_id): building_instance.delete() return delete_success() - @extend_schema( - responses={200: BuildingSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingSerializer, 400: None}) def patch(self, request, building_id): """ Edit building with given ID @@ -112,10 +100,7 @@ def get(self, request): class BuildingOwnerView(APIView): serializer_class = BuildingSerializer - @extend_schema( - responses={200: BuildingSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingSerializer, 400: None}) def get(self, request, owner_id): """ Get all buildings owned by syndic with given id diff --git a/backend/building_comment/apps.py b/backend/building_comment/apps.py index 13fa8e06..c01948e8 100644 --- a/backend/building_comment/apps.py +++ b/backend/building_comment/apps.py @@ -2,5 +2,5 @@ class BuildingCommentConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'building_comment' + default_auto_field = "django.db.models.BigAutoField" + name = "building_comment" diff --git a/backend/building_comment/urls.py b/backend/building_comment/urls.py index fb96cbe0..0ac242c5 100644 --- a/backend/building_comment/urls.py +++ b/backend/building_comment/urls.py @@ -4,12 +4,12 @@ DefaultBuildingComment, BuildingCommentIndividualView, BuildingCommentAllView, - BuildingCommentBuildingView + BuildingCommentBuildingView, ) urlpatterns = [ - path('/', BuildingCommentIndividualView.as_view()), - path('building//', BuildingCommentBuildingView.as_view()), - path('all/', BuildingCommentAllView.as_view()), - path('', DefaultBuildingComment.as_view()) + path("/", BuildingCommentIndividualView.as_view()), + path("building//", BuildingCommentBuildingView.as_view()), + path("all/", BuildingCommentAllView.as_view()), + path("", DefaultBuildingComment.as_view()), ] diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index 66d5d928..dd3d0137 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -11,10 +11,7 @@ class DefaultBuildingComment(APIView): serializer_class = BuildingCommentSerializer - @extend_schema( - responses={201: BuildingCommentSerializer, - 400: None} - ) + @extend_schema(responses={201: BuildingCommentSerializer, 400: None}) def post(self, request): """ Create a new BuildingComment @@ -34,30 +31,28 @@ def post(self, request): class BuildingCommentIndividualView(APIView): serializer_class = BuildingCommentSerializer - @extend_schema( - responses={200: BuildingCommentSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingCommentSerializer, 400: None}) def get(self, request, building_comment_id): """ Get an invividual BuildingComment with given id """ - building_comment_instance = BuildingComment.objects.filter(id=building_comment_id) + building_comment_instance = BuildingComment.objects.filter( + id=building_comment_id + ) if not building_comment_instance: return bad_request("BuildingComment") return get_success(BuildingCommentSerializer(building_comment_instance[0])) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, building_comment_id): """ Delete a BuildingComment with given id """ - building_comment_instance = BuildingComment.objectts.filter(id=building_comment_id) + building_comment_instance = BuildingComment.objectts.filter( + id=building_comment_id + ) if not building_comment_instance: return bad_request("BuildingComment") @@ -65,15 +60,14 @@ def delete(self, request, building_comment_id): building_comment_instance[0].delete() return delete_success() - @extend_schema( - responses={200: BuildingCommentSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingCommentSerializer, 400: None}) def patch(self, request, building_comment_id): """ Edit BuildingComment with given id """ - building_comment_instance = BuildingComment.objects.filter(id=building_comment_id) + building_comment_instance = BuildingComment.objects.filter( + id=building_comment_id + ) if not building_comment_instance: return bad_request("BuildingComment") @@ -92,15 +86,14 @@ def patch(self, request, building_comment_id): class BuildingCommentBuildingView(APIView): serializer_class = BuildingCommentSerializer - @extend_schema( - responses={200: BuildingCommentSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingCommentSerializer, 400: None}) def get(self, request, building_id): """ Get all BuildingComments of building with given building id """ - building_comment_instance = BuildingComment.objects.filter(building_id=building_id) + building_comment_instance = BuildingComment.objects.filter( + building_id=building_id + ) if not building_comment_instance: return bad_request_relation("BuildingComment", "building") diff --git a/backend/building_on_tour/apps.py b/backend/building_on_tour/apps.py index d702dbbc..a003e7cf 100644 --- a/backend/building_on_tour/apps.py +++ b/backend/building_on_tour/apps.py @@ -2,5 +2,5 @@ class BuildingOnTourConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'building_on_tour' + default_auto_field = "django.db.models.BigAutoField" + name = "building_on_tour" diff --git a/backend/building_on_tour/urls.py b/backend/building_on_tour/urls.py index 89cab323..8bb25d7c 100644 --- a/backend/building_on_tour/urls.py +++ b/backend/building_on_tour/urls.py @@ -1,13 +1,9 @@ from django.urls import path -from .views import ( - BuildingTourIndividualView, - AllBuildingToursView, - Default -) +from .views import BuildingTourIndividualView, AllBuildingToursView, Default urlpatterns = [ - path('/', BuildingTourIndividualView.as_view()), - path('all/', AllBuildingToursView.as_view()), - path('', Default.as_view()) + path("/", BuildingTourIndividualView.as_view()), + path("all/", AllBuildingToursView.as_view()), + path("", Default.as_view()), ] diff --git a/backend/building_on_tour/views.py b/backend/building_on_tour/views.py index f1f86924..5739db37 100644 --- a/backend/building_on_tour/views.py +++ b/backend/building_on_tour/views.py @@ -11,10 +11,7 @@ class Default(APIView): serializer_class = BuildingTourSerializer - @extend_schema( - responses={201: BuildingTourSerializer, - 400: None} - ) + @extend_schema(responses={201: BuildingTourSerializer, 400: None}) def post(self, request): """ Create a new BuildingOnTour with data from post @@ -34,10 +31,7 @@ def post(self, request): class BuildingTourIndividualView(APIView): serializer_class = BuildingTourSerializer - @extend_schema( - responses={200: BuildingTourSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingTourSerializer, 400: None}) def get(self, request, building_tour_id): """ Get info about a BuildingOnTour with given id @@ -50,10 +44,7 @@ def get(self, request, building_tour_id): serializer = BuildingTourSerializer(building_on_tour_instance[0]) return get_success(serializer) - @extend_schema( - responses={200: BuildingTourSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingTourSerializer, 400: None}) def patch(self, request, building_tour_id): """ edit info about a BuildingOnTour with given id @@ -74,10 +65,7 @@ def patch(self, request, building_tour_id): return patch_success(BuildingTourSerializer(building_on_tour_instance)) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, building_tour_id): """ delete a BuildingOnTour from the database diff --git a/backend/buildingurl/apps.py b/backend/buildingurl/apps.py index cee2b369..d50c713d 100644 --- a/backend/buildingurl/apps.py +++ b/backend/buildingurl/apps.py @@ -2,5 +2,5 @@ class BuildingurlConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'buildingurl' + default_auto_field = "django.db.models.BigAutoField" + name = "buildingurl" diff --git a/backend/buildingurl/urls.py b/backend/buildingurl/urls.py index dbb9a43a..7027d06c 100644 --- a/backend/buildingurl/urls.py +++ b/backend/buildingurl/urls.py @@ -5,13 +5,13 @@ BuildingUrlIndividualView, BuildingUrlSyndicView, BuildingUrlBuildingView, - BuildingUrlDefault + BuildingUrlDefault, ) urlpatterns = [ - path('all/', BuildingUrlAllView.as_view()), - path('/', BuildingUrlIndividualView.as_view()), - path('syndic//', BuildingUrlSyndicView.as_view()), - path('building//', BuildingUrlBuildingView.as_view()), - path('', BuildingUrlDefault.as_view()) + path("all/", BuildingUrlAllView.as_view()), + path("/", BuildingUrlIndividualView.as_view()), + path("syndic//", BuildingUrlSyndicView.as_view()), + path("building//", BuildingUrlBuildingView.as_view()), + path("", BuildingUrlDefault.as_view()), ] diff --git a/backend/buildingurl/views.py b/backend/buildingurl/views.py index b612d287..8a1412bf 100644 --- a/backend/buildingurl/views.py +++ b/backend/buildingurl/views.py @@ -11,10 +11,7 @@ class BuildingUrlDefault(APIView): serializer_class = BuildingUrlSerializer - @extend_schema( - responses={201: BuildingUrlSerializer, - 400: None} - ) + @extend_schema(responses={201: BuildingUrlSerializer, 400: None}) def post(self, request): """ Create a new building url @@ -45,10 +42,7 @@ def post(self, request): class BuildingUrlIndividualView(APIView): serializer_class = BuildingUrlSerializer - @extend_schema( - responses={200: BuildingUrlSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingUrlSerializer, 400: None}) def get(self, request, building_url_id): """ Get info about a buildingurl with given id @@ -60,10 +54,7 @@ def get(self, request, building_url_id): serializer = BuildingUrlSerializer(building_url_instance[0]) return get_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, building_url_id): """ Delete buildingurl with given id @@ -75,10 +66,7 @@ def delete(self, request, building_url_id): building_url_instance[0].delete() return delete_success() - @extend_schema( - responses={200: BuildingUrlSerializer, - 400: None} - ) + @extend_schema(responses={200: BuildingUrlSerializer, 400: None}) def patch(self, request, building_url_id): """ Edit info about buildingurl with given id @@ -103,6 +91,7 @@ class BuildingUrlSyndicView(APIView): """ /syndic/ """ + serializer_class = BuildingUrlSerializer def get(self, request, syndic_id): @@ -111,7 +100,9 @@ def get(self, request, syndic_id): """ # All building IDs where user is syndic - building_ids = [building.id for building in Building.objects.filter(syndic=syndic_id)] + building_ids = [ + building.id for building in Building.objects.filter(syndic=syndic_id) + ] building_urls_instances = BuildingURL.objects.filter(building__in=building_ids) serializer = BuildingUrlSerializer(building_urls_instances, many=True) @@ -122,6 +113,7 @@ class BuildingUrlBuildingView(APIView): """ building/ """ + serializer_class = BuildingUrlSerializer def get(self, request, building_id): diff --git a/backend/config/asgi.py b/backend/config/asgi.py index 9502b7fd..0fdc25c6 100644 --- a/backend/config/asgi.py +++ b/backend/config/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_asgi_application() diff --git a/backend/config/middleware.py b/backend/config/middleware.py index 6adbe572..2f380471 100644 --- a/backend/config/middleware.py +++ b/backend/config/middleware.py @@ -6,5 +6,5 @@ class CommonMiddlewareAppendSlashWithoutRedirect(CommonMiddleware): # However, Django likes slashes # This code adds a slash to URLs without a slash def process_request(self, request): - if not request.path.endswith('/'): - request.path_info = request.path_info + '/' + if not request.path.endswith("/"): + request.path_info = request.path_info + "/" diff --git a/backend/config/secrets.sample.py b/backend/config/secrets.sample.py index 64b1d268..cec3c21e 100644 --- a/backend/config/secrets.sample.py +++ b/backend/config/secrets.sample.py @@ -1,5 +1,5 @@ # This file is an example of how the secrets.py file should look like -DJANGO_SECRET_KEY = 'example-key-for-hashing' +DJANGO_SECRET_KEY = "example-key-for-hashing" -SECRET_EMAIL_USER = 'example@gmail.com' -SECRET_EMAIL_USER_PSWD = 'password' \ No newline at end of file +SECRET_EMAIL_USER = "example@gmail.com" +SECRET_EMAIL_USER_PSWD = "password" diff --git a/backend/config/settings.py b/backend/config/settings.py index 845a74f6..75d7cea3 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -25,144 +25,152 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['*', 'localhost', '127.0.0.1', '172.17.0.0'] +ALLOWED_HOSTS = ["*", "localhost", "127.0.0.1", "172.17.0.0"] # Application definition DJANGO_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.sites", ] AUTHENTICATION = [ - 'rest_framework.authtoken', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'dj_rest_auth', - 'dj_rest_auth.registration', - 'rest_framework_simplejwt.token_blacklist' + "rest_framework.authtoken", + "allauth", + "allauth.account", + "allauth.socialaccount", + "dj_rest_auth", + "dj_rest_auth.registration", + "rest_framework_simplejwt.token_blacklist", ] THIRD_PARTY_APPS = AUTHENTICATION + [ - 'corsheaders', - 'rest_framework', - 'phonenumber_field', - 'drf_spectacular', + "corsheaders", + "rest_framework", + "phonenumber_field", + "drf_spectacular", ] -CREATED_APPS = [ - 'base' -] +CREATED_APPS = ["base"] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CREATED_APPS REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", ], - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": ( + "dj_rest_auth.jwt_auth.JWTCookieAuthentication", ), - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } # drf-spectacular settings SPECTACULAR_SETTINGS = { - 'TITLE': 'Dr-Trottoir API', - 'DESCRIPTION': 'This is the documentation for the Dr-Trottoir API', - 'VERSION': '1.0.0', - 'SERVE_INCLUDE_SCHEMA': False, + "TITLE": "Dr-Trottoir API", + "DESCRIPTION": "This is the documentation for the Dr-Trottoir API", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, # OTHER SETTINGS } # authentication settings -AUTH_USER_MODEL = 'base.User' +AUTH_USER_MODEL = "base.User" REST_AUTH = { - 'USE_JWT': True, - 'JWT_AUTH_HTTPONLY': True, - 'JWT_AUTH_SAMESITE': 'Strict', - 'JWT_AUTH_COOKIE': 'auth-access-token', - 'JWT_AUTH_REFRESH_COOKIE': 'auth-refresh-token', + "USE_JWT": True, + "JWT_AUTH_HTTPONLY": True, + "JWT_AUTH_SAMESITE": "Strict", + "JWT_AUTH_COOKIE": "auth-access-token", + "JWT_AUTH_REFRESH_COOKIE": "auth-refresh-token", } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=14), - 'ROTATE_REFRESH_TOKENS': True, - 'BLACKLIST_AFTER_ROTATION': True, + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), + "REFRESH_TOKEN_LIFETIME": timedelta(days=14), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, } AUTHENTICATION_BACKENDS = [ - 'allauth.account.auth_backends.AuthenticationBackend', - 'django.contrib.auth.backends.ModelBackend', + "allauth.account.auth_backends.AuthenticationBackend", + "django.contrib.auth.backends.ModelBackend", ] # 'allauth' settings -ACCOUNT_AUTHENTICATION_METHOD = 'email' +ACCOUNT_AUTHENTICATION_METHOD = "email" ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_USERNAME_REQUIRED = False -ACCOUNT_EMAIL_VERIFICATION = 'optional' if DEBUG else 'mandatory' -LOGIN_URL = 'http://localhost:2002/user/login' +ACCOUNT_EMAIL_VERIFICATION = "optional" if DEBUG else "mandatory" +LOGIN_URL = "http://localhost:2002/user/login" SITE_ID = 1 MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'config.middleware.CommonMiddlewareAppendSlashWithoutRedirect', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "corsheaders.middleware.CorsMiddleware", + "config.middleware.CommonMiddlewareAppendSlashWithoutRedirect", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] CORS_ALLOW_ALL_ORIGINS = False CORS_ALLOW_CREDENTIALS = True -CORS_ALLOWED_ORIGINS = ['http://localhost:2002', 'http://localhost:443', 'http://localhost:80', "http://localhost"] -CSRF_TRUSTED_ORIGINS = ['http://localhost:2002', 'http://localhost:443', 'http://localhost:80', "http://localhost"] +CORS_ALLOWED_ORIGINS = [ + "http://localhost:2002", + "http://localhost:443", + "http://localhost:80", + "http://localhost", +] +CSRF_TRUSTED_ORIGINS = [ + "http://localhost:2002", + "http://localhost:443", + "http://localhost:80", + "http://localhost", +] -ROOT_URLCONF = 'config.urls' +ROOT_URLCONF = "config.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'config.wsgi.application' +WSGI_APPLICATION = "config.wsgi.application" # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'drtrottoir', - 'USER': 'django', - 'PASSWORD': 'password', + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "drtrottoir", + "USER": "django", + "PASSWORD": "password", # 'HOST': 'localhost', # If you want to run using python manage.py runserver - 'HOST': 'web', # If you want to use `docker-compose up` - 'PORT': '5432', + "HOST": "web", # If you want to use `docker-compose up` + "PORT": "5432", } } @@ -171,25 +179,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'CET' +TIME_ZONE = "CET" USE_I18N = True @@ -198,24 +206,24 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = "static/" # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Email -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_USE_TLS = True -EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST = "smtp.gmail.com" EMAIL_PORT = 587 EMAIL_HOST_USER = SECRET_EMAIL_USER EMAIL_HOST_PASSWORD = SECRET_EMAIL_USER_PSWD # Media -MEDIA_ROOT = '/app/media' -MEDIA_URL = '/media/' +MEDIA_ROOT = "/app/media" +MEDIA_URL = "/media/" # allow upload big file DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 20 # 20M diff --git a/backend/config/urls.py b/backend/config/urls.py index 731457a2..08c5ecda 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -35,21 +35,23 @@ from .settings import MEDIA_URL, MEDIA_ROOT urlpatterns = [ - path('admin/', admin.site.urls), - path('docs/', SpectacularAPIView.as_view(), name='schema'), - path('docs/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), - path('authentication/', include(authentication_urls)), - path('manual/', include(manual_urls)), - path('picture_building/', include(picture_building_urls)), - path('building/', include(building_urls)), - path('building_comment/', include(building_comment_urls)), - path('region/', include(region_urls)), - path('buildingurl/', include(building_url_urls)), - path('garbage_collection/', include(garbage_collection_urls)), - path('building_on_tour/', include(building_on_tour_urls)), - path('user/', include(user_urls)), - path('role/', include(role_urls)), - path('student_at_building_on_tour/', include(stud_buil_tour_urls)), - path('tour/', include(tour_urls)), - re_path(r'^$', RedirectView.as_view(url=reverse_lazy('api'), permanent=False)), - ] + static(MEDIA_URL, document_root=MEDIA_ROOT) + path("admin/", admin.site.urls), + path("docs/", SpectacularAPIView.as_view(), name="schema"), + path( + "docs/ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" + ), + path("authentication/", include(authentication_urls)), + path("manual/", include(manual_urls)), + path("picture_building/", include(picture_building_urls)), + path("building/", include(building_urls)), + path("building_comment/", include(building_comment_urls)), + path("region/", include(region_urls)), + path("buildingurl/", include(building_url_urls)), + path("garbage_collection/", include(garbage_collection_urls)), + path("building_on_tour/", include(building_on_tour_urls)), + path("user/", include(user_urls)), + path("role/", include(role_urls)), + path("student_at_building_on_tour/", include(stud_buil_tour_urls)), + path("tour/", include(tour_urls)), + re_path(r"^$", RedirectView.as_view(url=reverse_lazy("api"), permanent=False)), +] + static(MEDIA_URL, document_root=MEDIA_ROOT) diff --git a/backend/config/wsgi.py b/backend/config/wsgi.py index 3d2dc456..1ee1797e 100644 --- a/backend/config/wsgi.py +++ b/backend/config/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_wsgi_application() diff --git a/backend/garbage_collection/apps.py b/backend/garbage_collection/apps.py index 03373772..de2c391f 100644 --- a/backend/garbage_collection/apps.py +++ b/backend/garbage_collection/apps.py @@ -2,5 +2,5 @@ class GarbageCollectionConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'garbage_collection' + default_auto_field = "django.db.models.BigAutoField" + name = "garbage_collection" diff --git a/backend/garbage_collection/urls.py b/backend/garbage_collection/urls.py index ba6dba1f..009b3c57 100644 --- a/backend/garbage_collection/urls.py +++ b/backend/garbage_collection/urls.py @@ -4,12 +4,12 @@ GarbageCollectionIndividualView, GarbageCollectionIndividualBuildingView, DefaultGarbageCollection, - GarbageCollectionAllView + GarbageCollectionAllView, ) urlpatterns = [ - path('all/', GarbageCollectionAllView.as_view()), - path('building//', GarbageCollectionIndividualBuildingView.as_view()), - path('/', GarbageCollectionIndividualView.as_view()), - path('', DefaultGarbageCollection.as_view()) + path("all/", GarbageCollectionAllView.as_view()), + path("building//", GarbageCollectionIndividualBuildingView.as_view()), + path("/", GarbageCollectionIndividualView.as_view()), + path("", DefaultGarbageCollection.as_view()), ] diff --git a/backend/garbage_collection/views.py b/backend/garbage_collection/views.py index ef7a854e..21f31737 100644 --- a/backend/garbage_collection/views.py +++ b/backend/garbage_collection/views.py @@ -7,13 +7,11 @@ TRANSLATE = {"building": "building_id"} + class DefaultGarbageCollection(APIView): serializer_class = GarbageCollectionSerializer - @extend_schema( - responses={201: GarbageCollectionSerializer, - 400: None} - ) + @extend_schema(responses={201: GarbageCollectionSerializer, 400: None}) def post(self, request): """ Create new garbage collection @@ -34,43 +32,40 @@ def post(self, request): class GarbageCollectionIndividualView(APIView): serializer_class = GarbageCollectionSerializer - @extend_schema( - responses={200: GarbageCollectionSerializer, - 400: None} - ) + @extend_schema(responses={200: GarbageCollectionSerializer, 400: None}) def get(self, request, garbage_collection_id): """ Get info about a garbage collection with given id """ - garbage_collection_instance = GarbageCollection.objects.filter(id=garbage_collection_id) + garbage_collection_instance = GarbageCollection.objects.filter( + id=garbage_collection_id + ) if not garbage_collection_instance: return bad_request("GarbageCollection") serializer = GarbageCollectionSerializer(garbage_collection_instance[0]) return get_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, garbage_collection_id): """ Delete garbage collection with given id """ - garbage_collection_instance = GarbageCollection.objects.filter(id=garbage_collection_id) + garbage_collection_instance = GarbageCollection.objects.filter( + id=garbage_collection_id + ) if not garbage_collection_instance: return bad_request("GarbageCollection") garbage_collection_instance[0].delete() return delete_success() - @extend_schema( - responses={200: GarbageCollectionSerializer, - 400: None} - ) + @extend_schema(responses={200: GarbageCollectionSerializer, 400: None}) def patch(self, request, garbage_collection_id): """ Edit garbage collection with given id """ - garbage_collection_instance = GarbageCollection.objects.filter(id=garbage_collection_id) + garbage_collection_instance = GarbageCollection.objects.filter( + id=garbage_collection_id + ) if not garbage_collection_instance: return bad_request("GarbageCollection") @@ -90,14 +85,19 @@ class GarbageCollectionIndividualBuildingView(APIView): """ /building/ """ + serializer_class = GarbageCollectionSerializer def get(self, request, building_id): """ Get info about all garbage collections of a building with given id """ - garbage_collection_instances = GarbageCollection.objects.filter(building=building_id) - serializer = GarbageCollectionSerializer(garbage_collection_instances, many=True) + garbage_collection_instances = GarbageCollection.objects.filter( + building=building_id + ) + serializer = GarbageCollectionSerializer( + garbage_collection_instances, many=True + ) return get_success(serializer) @@ -109,5 +109,7 @@ def get(self, request): Get all garbage collections """ garbage_collection_instances = GarbageCollection.objects.all() - serializer = GarbageCollectionSerializer(garbage_collection_instances, many=True) + serializer = GarbageCollectionSerializer( + garbage_collection_instances, many=True + ) return get_success(serializer) diff --git a/backend/manage.py b/backend/manage.py index 8e7ac79b..d28672ea 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/backend/manual/apps.py b/backend/manual/apps.py index 327f081b..294880fc 100644 --- a/backend/manual/apps.py +++ b/backend/manual/apps.py @@ -2,5 +2,5 @@ class ManualConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'manual' + default_auto_field = "django.db.models.BigAutoField" + name = "manual" diff --git a/backend/manual/urls.py b/backend/manual/urls.py index 400911ef..bd072321 100644 --- a/backend/manual/urls.py +++ b/backend/manual/urls.py @@ -1,15 +1,10 @@ from django.urls import path -from .views import ( - ManualView, - ManualBuildingView, - ManualsView, - Default -) +from .views import ManualView, ManualBuildingView, ManualsView, Default urlpatterns = [ - path('/', ManualView.as_view()), - path('building//', ManualBuildingView.as_view()), - path('all/', ManualsView.as_view()), - path('', Default.as_view()) + path("/", ManualView.as_view()), + path("building//", ManualBuildingView.as_view()), + path("all/", ManualsView.as_view()), + path("", Default.as_view()), ] diff --git a/backend/manual/views.py b/backend/manual/views.py index 54fec412..0b20bd5e 100644 --- a/backend/manual/views.py +++ b/backend/manual/views.py @@ -11,10 +11,7 @@ class Default(APIView): serializer_class = ManualSerializer - @extend_schema( - responses={201: ManualSerializer, - 400: None} - ) + @extend_schema(responses={201: ManualSerializer, 400: None}) def post(self, request): """ Create a new manual with data from post @@ -33,10 +30,7 @@ def post(self, request): class ManualView(APIView): serializer_class = ManualSerializer - @extend_schema( - responses={200: ManualSerializer, - 400: None} - ) + @extend_schema(responses={200: ManualSerializer, 400: None}) def get(self, request, manual_id): """ Get info about a manual with given id @@ -47,10 +41,7 @@ def get(self, request, manual_id): serializer = ManualSerializer(manual_instances[0]) return get_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, manual_id): """ Delete manual with given id @@ -61,10 +52,7 @@ def delete(self, request, manual_id): manual_instances[0].delete() return delete_success() - @extend_schema( - responses={200: ManualSerializer, - 400: None} - ) + @extend_schema(responses={200: ManualSerializer, 400: None}) def patch(self, request, manual_id): """ Edit info about a manual with given id @@ -86,10 +74,7 @@ def patch(self, request, manual_id): class ManualBuildingView(APIView): serializer_class = ManualSerializer - @extend_schema( - responses={200: ManualSerializer, - 400: None} - ) + @extend_schema(responses={200: ManualSerializer, 400: None}) def get(self, request, building_id): """ Get all manuals of a building with given id diff --git a/backend/picture_building/apps.py b/backend/picture_building/apps.py index 43ea1414..a71bdca5 100644 --- a/backend/picture_building/apps.py +++ b/backend/picture_building/apps.py @@ -2,5 +2,5 @@ class PictureBuildingConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'picture_building' + default_auto_field = "django.db.models.BigAutoField" + name = "picture_building" diff --git a/backend/picture_building/urls.py b/backend/picture_building/urls.py index c89ad4e3..2340819e 100644 --- a/backend/picture_building/urls.py +++ b/backend/picture_building/urls.py @@ -4,12 +4,12 @@ PictureBuildingIndividualView, PicturesOfBuildingView, AllPictureBuildingsView, - Default + Default, ) urlpatterns = [ - path('/', PictureBuildingIndividualView.as_view()), - path('building//', PicturesOfBuildingView.as_view()), - path('all/', AllPictureBuildingsView.as_view()), - path('', Default.as_view()) + path("/", PictureBuildingIndividualView.as_view()), + path("building//", PicturesOfBuildingView.as_view()), + path("all/", AllPictureBuildingsView.as_view()), + path("", Default.as_view()), ] diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 76740c94..ab9af34b 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -11,10 +11,7 @@ class Default(APIView): serializer_class = PictureBuildingSerializer - @extend_schema( - responses={201: PictureBuildingSerializer, - 400: None} - ) + @extend_schema(responses={201: PictureBuildingSerializer, 400: None}) def post(self, request): """ Create a new PictureBuilding @@ -33,30 +30,28 @@ def post(self, request): class PictureBuildingIndividualView(APIView): serializer_class = PictureBuildingSerializer - @extend_schema( - responses={200: PictureBuildingSerializer, - 400: None} - ) + @extend_schema(responses={200: PictureBuildingSerializer, 400: None}) def get(self, request, picture_building_id): """ Get PictureBuilding with given id """ - picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) + picture_building_instance = PictureBuilding.objects.filter( + id=picture_building_id + ) if len(picture_building_instance) != 1: return bad_request("PictureBuilding") serializer = PictureBuildingSerializer(picture_building_instance[0]) return get_success(serializer) - @extend_schema( - responses={200: PictureBuildingSerializer, - 400: None} - ) + @extend_schema(responses={200: PictureBuildingSerializer, 400: None}) def patch(self, request, picture_building_id): """ Edit info about PictureBuilding with given id """ - picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) + picture_building_instance = PictureBuilding.objects.filter( + id=picture_building_id + ) if not picture_building_instance: return bad_request("PictureBuilding") @@ -71,15 +66,14 @@ def patch(self, request, picture_building_id): return patch_success(PictureBuildingSerializer(picture_building_instance)) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, picture_building_id): """ delete a pictureBuilding from the database """ - picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) + picture_building_instance = PictureBuilding.objects.filter( + id=picture_building_id + ) if len(picture_building_instance) != 1: return bad_request("PictureBuilding") picture_building_instance[0].delete() @@ -93,7 +87,9 @@ def get(self, request, building_id): """ Get all pictures of a building with given id """ - picture_building_instances = PictureBuilding.objects.filter(building_id=building_id) + picture_building_instances = PictureBuilding.objects.filter( + building_id=building_id + ) serializer = PictureBuildingSerializer(picture_building_instances, many=True) return get_success(serializer) diff --git a/backend/region/apps.py b/backend/region/apps.py index 4dc60ec4..ec013ca3 100644 --- a/backend/region/apps.py +++ b/backend/region/apps.py @@ -2,5 +2,5 @@ class RegionConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'region' + default_auto_field = "django.db.models.BigAutoField" + name = "region" diff --git a/backend/region/urls.py b/backend/region/urls.py index 8888c868..ab0aca4c 100644 --- a/backend/region/urls.py +++ b/backend/region/urls.py @@ -1,13 +1,9 @@ from django.urls import path -from .views import ( - RegionIndividualView, - AllRegionsView, - Default -) +from .views import RegionIndividualView, AllRegionsView, Default urlpatterns = [ - path('/', RegionIndividualView.as_view()), - path('all/', AllRegionsView.as_view()), - path('', Default.as_view()) + path("/", RegionIndividualView.as_view()), + path("all/", AllRegionsView.as_view()), + path("", Default.as_view()), ] diff --git a/backend/region/views.py b/backend/region/views.py index 7b3f9774..563e8a08 100644 --- a/backend/region/views.py +++ b/backend/region/views.py @@ -12,10 +12,7 @@ class Default(APIView): permission_classes = [permissions.IsAuthenticated] - @extend_schema( - responses={201: RegionSerializer, - 400: None} - ) + @extend_schema(responses={201: RegionSerializer, 400: None}) def post(self, request): """ Create a new region @@ -38,10 +35,7 @@ class RegionIndividualView(APIView): permission_classes = [permissions.IsAuthenticated] - @extend_schema( - responses={200: RegionSerializer, - 400: None} - ) + @extend_schema(responses={200: RegionSerializer, 400: None}) def get(self, request, region_id): """ Get info about a Region with given id @@ -54,10 +48,7 @@ def get(self, request, region_id): serializer = RegionSerializer(region_instance[0]) return get_success(serializer) - @extend_schema( - responses={200: RegionSerializer, - 400: None} - ) + @extend_schema(responses={200: RegionSerializer, 400: None}) def patch(self, request, region_id): """ Edit Region with given id @@ -78,11 +69,7 @@ def patch(self, request, region_id): return patch_success(RegionSerializer(region_instance)) - - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, region_id): """ delete a region with given id diff --git a/backend/role/apps.py b/backend/role/apps.py index 2857409d..a0fb6510 100644 --- a/backend/role/apps.py +++ b/backend/role/apps.py @@ -2,5 +2,5 @@ class RoleConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'role' + default_auto_field = "django.db.models.BigAutoField" + name = "role" diff --git a/backend/role/urls.py b/backend/role/urls.py index 5d308001..a968da32 100644 --- a/backend/role/urls.py +++ b/backend/role/urls.py @@ -1,14 +1,10 @@ from django.urls import path -from .views import ( - RoleIndividualView, - AllRolesView, - DefaultRoleView -) +from .views import RoleIndividualView, AllRolesView, DefaultRoleView urlpatterns = [ - path('/', RoleIndividualView.as_view()), - path('all/', AllRolesView.as_view()), - path('', DefaultRoleView.as_view()) + path("/", RoleIndividualView.as_view()), + path("all/", AllRolesView.as_view()), + path("", DefaultRoleView.as_view()), ] diff --git a/backend/role/views.py b/backend/role/views.py index 8a8ce545..04e1eacf 100644 --- a/backend/role/views.py +++ b/backend/role/views.py @@ -9,10 +9,7 @@ class DefaultRoleView(APIView): serializer_class = RoleSerializer - @extend_schema( - responses={201: RoleSerializer, - 400: None} - ) + @extend_schema(responses={201: RoleSerializer, 400: None}) def post(self, request): """ Create a new role @@ -32,10 +29,7 @@ def post(self, request): class RoleIndividualView(APIView): serializer_class = RoleSerializer - @extend_schema( - responses={200: RoleSerializer, - 400: None} - ) + @extend_schema(responses={200: RoleSerializer, 400: None}) def get(self, request, role_id): """ Get info about a Role with given id @@ -48,10 +42,7 @@ def get(self, request, role_id): serializer = RoleSerializer(role_instance[0]) return get_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, role_id): """ Delete a Role with given id @@ -64,10 +55,7 @@ def delete(self, request, role_id): role_instance[0].delete() return delete_success() - @extend_schema( - responses={200: RoleSerializer, - 400: None} - ) + @extend_schema(responses={200: RoleSerializer, 400: None}) def patch(self, request, role_id): """ Edit info about a Role with given id diff --git a/backend/student_at_building_on_tour/apps.py b/backend/student_at_building_on_tour/apps.py index 71bda9aa..8aae98d7 100644 --- a/backend/student_at_building_on_tour/apps.py +++ b/backend/student_at_building_on_tour/apps.py @@ -2,5 +2,5 @@ class StudentAtBuildingOnTourConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'student_at_building_on_tour' + default_auto_field = "django.db.models.BigAutoField" + name = "student_at_building_on_tour" diff --git a/backend/student_at_building_on_tour/urls.py b/backend/student_at_building_on_tour/urls.py index ef70f54f..27531826 100644 --- a/backend/student_at_building_on_tour/urls.py +++ b/backend/student_at_building_on_tour/urls.py @@ -4,12 +4,15 @@ StudentAtBuildingOnTourIndividualView, BuildingTourPerStudentView, AllView, - Default + Default, ) urlpatterns = [ - path('/', StudentAtBuildingOnTourIndividualView.as_view()), - path('student//', BuildingTourPerStudentView.as_view()), - path('all/', AllView.as_view()), - path('', Default.as_view()) + path( + "/", + StudentAtBuildingOnTourIndividualView.as_view(), + ), + path("student//", BuildingTourPerStudentView.as_view()), + path("all/", AllView.as_view()), + path("", Default.as_view()), ] diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index 58f4207b..b73080ab 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -11,10 +11,7 @@ class Default(APIView): serializer_class = StudBuildTourSerializer - @extend_schema( - responses={201: StudBuildTourSerializer, - 400: None} - ) + @extend_schema(responses={201: StudBuildTourSerializer, 400: None}) def post(self, request): """ Create a new StudentAtBuildingOnTour @@ -27,7 +24,9 @@ def post(self, request): if r := try_full_clean_and_save(student_at_building_on_tour_instance): return r - return post_success(StudBuildTourSerializer(student_at_building_on_tour_instance)) + return post_success( + StudBuildTourSerializer(student_at_building_on_tour_instance) + ) class BuildingTourPerStudentView(APIView): @@ -37,23 +36,26 @@ def get(self, request, student_id): """ Get all StudentAtBuildingOnTour for a student with given id """ - student_at_building_on_tour_instances = StudentAtBuildingOnTour.objects.filter(student_id=student_id) - serializer = StudBuildTourSerializer(student_at_building_on_tour_instances, many=True) + student_at_building_on_tour_instances = StudentAtBuildingOnTour.objects.filter( + student_id=student_id + ) + serializer = StudBuildTourSerializer( + student_at_building_on_tour_instances, many=True + ) return get_success(serializer) class StudentAtBuildingOnTourIndividualView(APIView): serializer_class = StudBuildTourSerializer - @extend_schema( - responses={200: StudBuildTourSerializer, - 400: None} - ) + @extend_schema(responses={200: StudBuildTourSerializer, 400: None}) def get(self, request, student_at_building_on_tour_id): """ Get an individual StudentAtBuildingOnTour with given id """ - stud_tour_building_instance = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) + stud_tour_building_instance = StudentAtBuildingOnTour.objects.filter( + id=student_at_building_on_tour_id + ) if len(stud_tour_building_instance) != 1: return bad_request("StudentAtBuildingOnTour") @@ -61,15 +63,14 @@ def get(self, request, student_at_building_on_tour_id): serializer = StudBuildTourSerializer(stud_tour_building_instance[0]) return get_success(serializer) - @extend_schema( - responses={200: StudBuildTourSerializer, - 400: None} - ) + @extend_schema(responses={200: StudBuildTourSerializer, 400: None}) def patch(self, request, student_at_building_on_tour_id): """ Edit info about an individual StudentAtBuildingOnTour with given id """ - stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) + stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter( + id=student_at_building_on_tour_id + ) if len(stud_tour_building_instances) != 1: return bad_request("StudentAtBuildingOnTour") @@ -86,15 +87,14 @@ def patch(self, request, student_at_building_on_tour_id): serializer = StudBuildTourSerializer(stud_tour_building_instance) return patch_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, student_at_building_on_tour_id): """ Delete StudentAtBuildingOnTour with given id """ - stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) + stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter( + id=student_at_building_on_tour_id + ) if len(stud_tour_building_instances) != 1: return bad_request("StudentAtBuildingOnTour") stud_tour_building_instances[0].delete() diff --git a/backend/tour/apps.py b/backend/tour/apps.py index 1b9c0b3a..bfd89b32 100644 --- a/backend/tour/apps.py +++ b/backend/tour/apps.py @@ -2,5 +2,5 @@ class TourConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'tour' + default_auto_field = "django.db.models.BigAutoField" + name = "tour" diff --git a/backend/tour/urls.py b/backend/tour/urls.py index 53af4c67..2fc8c8f4 100644 --- a/backend/tour/urls.py +++ b/backend/tour/urls.py @@ -1,13 +1,9 @@ from django.urls import path -from .views import ( - TourIndividualView, - AllToursView, - Default -) +from .views import TourIndividualView, AllToursView, Default urlpatterns = [ - path('/', TourIndividualView.as_view()), - path('all/', AllToursView.as_view()), - path('', Default.as_view()) + path("/", TourIndividualView.as_view()), + path("all/", AllToursView.as_view()), + path("", Default.as_view()), ] diff --git a/backend/tour/views.py b/backend/tour/views.py index cc346889..bd8625b1 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -13,10 +13,7 @@ class Default(APIView): permission_classes = [permissions.IsAuthenticated] serializer_class = TourSerializer - @extend_schema( - responses={201: TourSerializer, - 400: None} - ) + @extend_schema(responses={201: TourSerializer, 400: None}) def post(self, request): """ Create a new tour @@ -35,10 +32,7 @@ def post(self, request): class TourIndividualView(APIView): serializer_class = TourSerializer - @extend_schema( - responses={200: TourSerializer, - 400: None} - ) + @extend_schema(responses={200: TourSerializer, 400: None}) def get(self, request, tour_id): """ Get info about a Tour with given id @@ -52,11 +46,7 @@ def get(self, request, tour_id): serializer = TourSerializer(tour_instance) return get_success(serializer) - - @extend_schema( - responses={200: TourSerializer, - 400: None} - ) + @extend_schema(responses={200: TourSerializer, 400: None}) def patch(self, request, tour_id): """ Edit a tour with given id @@ -76,10 +66,7 @@ def patch(self, request, tour_id): return patch_success(TourSerializer(tour_instance)) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, tour_id): """ Delete a tour with given id diff --git a/backend/users/managers.py b/backend/users/managers.py index 4c644326..413d416e 100644 --- a/backend/users/managers.py +++ b/backend/users/managers.py @@ -15,9 +15,7 @@ def create_user(self, email, password, **extra_fields): if not email: raise ValueError(_("Email is required")) email = self.normalize_email(email) - user = self.model( - email=email, - **extra_fields) + user = self.model(email=email, **extra_fields) user.set_password(password) user.save() return user diff --git a/backend/users/tests.py b/backend/users/tests.py index d5b2fcec..f9717bba 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -3,7 +3,6 @@ class UsersManagersTests(TestCase): - def test_create_user(self): User = get_user_model() user = User.objects.create_user(email="normal@user.com", password="foo") @@ -26,7 +25,9 @@ def test_create_user(self): def test_create_superuser(self): User = get_user_model() - admin_user = User.objects.create_superuser(email="super@user.com", password="foo") + admin_user = User.objects.create_superuser( + email="super@user.com", password="foo" + ) self.assertEqual(admin_user.email, "super@user.com") self.assertTrue(admin_user.is_active) self.assertTrue(admin_user.is_staff) @@ -39,4 +40,5 @@ def test_create_superuser(self): pass with self.assertRaises(ValueError): User.objects.create_superuser( - email="super@user.com", password="foo", is_superuser=False) + email="super@user.com", password="foo", is_superuser=False + ) diff --git a/backend/users/urls.py b/backend/users/urls.py index 41ba50d3..fa04b59b 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,13 +1,9 @@ from django.urls import path -from .views import ( - UserIndividualView, - AllUsersView, - DefaultUser -) +from .views import UserIndividualView, AllUsersView, DefaultUser urlpatterns = [ - path('/', UserIndividualView.as_view()), - path('all/', AllUsersView.as_view()), - path('', DefaultUser.as_view()) + path("/", UserIndividualView.as_view()), + path("all/", AllUsersView.as_view()), + path("", DefaultUser.as_view()), ] diff --git a/backend/users/views.py b/backend/users/views.py index dd40c5e6..1d244af5 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -11,6 +11,7 @@ TRANSLATE = {"role": "role_id"} + # In GET, you only get active users # Except when you explicitly pass a parameter 'include_inactive' to the body of the request and set it as true # If you @@ -36,10 +37,7 @@ class DefaultUser(APIView): # TODO: in order for this to work, you have to pass a password # In the future, we probably won't use POST this way anymore (if we work with the whitelist method) - @extend_schema( - responses={201: UserSerializer, - 400: None} - ) + @extend_schema(responses={201: UserSerializer, 400: None}) def post(self, request): """ Create a new user @@ -69,10 +67,7 @@ class UserIndividualView(APIView): serializer_class = UserSerializer permission_classes = [permissions.IsAuthenticated] - @extend_schema( - responses={200: UserSerializer, - 400: None} - ) + @extend_schema(responses={200: UserSerializer, 400: None}) def get(self, request, user_id): """ Get info about user with given id @@ -85,10 +80,7 @@ def get(self, request, user_id): serializer = UserSerializer(user_instance[0]) return get_success(serializer) - @extend_schema( - responses={204: None, - 400: None} - ) + @extend_schema(responses={204: None, 400: None}) def delete(self, request, user_id): """ Delete user with given id @@ -105,10 +97,7 @@ def delete(self, request, user_id): return delete_success() - @extend_schema( - responses={200: UserSerializer, - 400: None} - ) + @extend_schema(responses={200: UserSerializer, 400: None}) def patch(self, request, user_id): """ Edit user with given id diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index ae2b6d85..919f7fd7 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -19,14 +19,14 @@ def set_keys_of_instance(instance, data: dict, translation: dict = {}): def bad_request(object_name="Object"): return Response( {"res", f"{object_name} with given ID does not exist."}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) def bad_request_relation(object1: str, object2: str): return Response( {"res", f"There is no {object1} that is linked to {object2} with given id."}, - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) @@ -40,8 +40,10 @@ def try_full_clean_and_save(model_instance, rm=False): except AttributeError as e: # If body is empty, an attribute error is thrown in the clean function # if there is not checked whether the fields in self are intialized - error_message = str(e) + \ - ". This error could be thrown after you passed an empty body with e.g. a POST request." + error_message = ( + str(e) + + ". This error could be thrown after you passed an empty body with e.g. a POST request." + ) except (IntegrityError, ObjectDoesNotExist, ValueError) as e: error_message = str(e) finally: diff --git a/frontend/components/header/BaseHeader.tsx b/frontend/components/header/BaseHeader.tsx index be3f05ce..04b6d55f 100644 --- a/frontend/components/header/BaseHeader.tsx +++ b/frontend/components/header/BaseHeader.tsx @@ -1,21 +1,16 @@ -import React from 'react'; -import Image from 'next/image' -import logo from '../../public/logo.png' -import styles from './BaseHeader.module.css'; +import React from "react"; +import Image from "next/image"; +import logo from "../../public/logo.png"; +import styles from "./BaseHeader.module.css"; const BaseHeader = () => { - return ( -
-
- My App Logo -
-
- ); + return ( +
+
+ My App Logo +
+
+ ); }; -export default BaseHeader; \ No newline at end of file +export default BaseHeader; diff --git a/frontend/context/AuthProvider.tsx b/frontend/context/AuthProvider.tsx index 0998b26b..94b81e28 100644 --- a/frontend/context/AuthProvider.tsx +++ b/frontend/context/AuthProvider.tsx @@ -1,46 +1,43 @@ -import {createContext, ReactNode, useEffect, useState} from "react"; +import { createContext, ReactNode, useEffect, useState } from "react"; const AuthContext = createContext({ - auth: false, - loginUser: () => {}, - logoutUser: () => {} + auth: false, + loginUser: () => {}, + logoutUser: () => {}, }); export default AuthContext; -export const AuthProvider = ({children}: { children: ReactNode }) => { - const [auth, setAuth] = useState(false); +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [auth, setAuth] = useState(false); - let loginUser = async () => { - setAuth(true); - } - - let logoutUser = async () => { - setAuth(false); - sessionStorage.removeItem('auth'); - } + let loginUser = async () => { + setAuth(true); + }; - useEffect(() => { - const data = sessionStorage.getItem('auth'); - if (data) { - setAuth(JSON.parse(data)); - } - }, []); + let logoutUser = async () => { + setAuth(false); + sessionStorage.removeItem("auth"); + }; - useEffect(() => { - sessionStorage.setItem('auth', JSON.stringify(auth)); - }, [auth]); - - - let contextData = { - auth: auth, - loginUser: loginUser, - logoutUser: logoutUser, + useEffect(() => { + const data = sessionStorage.getItem("auth"); + if (data) { + setAuth(JSON.parse(data)); } - - return ( - - {children} - - ); -}; \ No newline at end of file + }, []); + + useEffect(() => { + sessionStorage.setItem("auth", JSON.stringify(auth)); + }, [auth]); + + let contextData = { + auth: auth, + loginUser: loginUser, + logoutUser: logoutUser, + }; + + return ( + {children} + ); +}; diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index c9b53574..514650bc 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -1,28 +1,33 @@ -import api from '../pages/api/axios'; -import {Login} from "@/types.d"; -import {NextRouter} from "next/router"; +import api from "../pages/api/axios"; +import { Login } from "@/types.d"; +import { NextRouter } from "next/router"; -const login = async (email: string, password: string, router: NextRouter, loginUser: () => void): Promise => { +const login = async ( + email: string, + password: string, + router: NextRouter, + loginUser: () => void +): Promise => { + const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; + const login_data: Login = { + email: email, + password: password, + }; - const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; - const login_data: Login = { - email: email, - password: password, - }; - - // Attempt to login with axios so authentication tokens get saved in our axios instance - api.post(host, login_data, { - headers: {'Content-Type': 'application/json'} + // Attempt to login with axios so authentication tokens get saved in our axios instance + api + .post(host, login_data, { + headers: { "Content-Type": "application/json" }, + }) + .then((response: { status: number }) => { + if (response.status == 200) { + loginUser(); + router.push("/welcome"); + } }) - .then((response: { status: number }) => { - if (response.status == 200) { - loginUser(); - router.push('/welcome'); - } - }) - .catch((error) => { - console.error(error); - }); + .catch((error) => { + console.error(error); + }); }; -export default login; \ No newline at end of file +export default login; diff --git a/frontend/lib/reset.tsx b/frontend/lib/reset.tsx index f4feb160..6653b98e 100644 --- a/frontend/lib/reset.tsx +++ b/frontend/lib/reset.tsx @@ -1,28 +1,29 @@ -import {Reset_Password} from "@/types.d"; -import {NextRouter} from "next/router"; +import { Reset_Password } from "@/types.d"; +import { NextRouter } from "next/router"; const reset = async (email: string, router: NextRouter): Promise => { + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}`; + const reset_data: Reset_Password = { + email: email, + }; - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}` - const reset_data: Reset_Password = { - email: email, - } - - try { - // Request without axios because no authentication is needed for this POST request - const response = await fetch(host, { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(reset_data), - }); + try { + // Request without axios because no authentication is needed for this POST request + const response = await fetch(host, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(reset_data), + }); - if (response.status == 200) { - alert("A password reset e-mail has been sent to the provided e-mail address"); - await router.push('/login'); - } - } catch (error) { - console.error(error); + if (response.status == 200) { + alert( + "A password reset e-mail has been sent to the provided e-mail address" + ); + await router.push("/login"); } -} + } catch (error) { + console.error(error); + } +}; -export default reset; \ No newline at end of file +export default reset; diff --git a/frontend/lib/signup.tsx b/frontend/lib/signup.tsx index b036cc1b..5e0d7d94 100644 --- a/frontend/lib/signup.tsx +++ b/frontend/lib/signup.tsx @@ -1,37 +1,43 @@ -import {SignUp} from "@/types.d"; +import { SignUp } from "@/types.d"; -const signup = async (firstname: string, lastname: string, email: string, password1: string, password2: string, router: any): Promise => { +const signup = async ( + firstname: string, + lastname: string, + email: string, + password1: string, + password2: string, + router: any +): Promise => { + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; + const signup_data: SignUp = { + first_name: firstname, + last_name: lastname, + email: email, + password1: password1, + password2: password2, + }; - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; - const signup_data: SignUp = { - first_name: firstname, - last_name: lastname, - email: email, - password1: password1, - password2: password2, - } - - // TODO Display error message from backend that will check this - // Small check if passwords are equal - if (signup_data.password1 !== signup_data.password2) { - alert("Passwords do not match"); - return; - } + // TODO Display error message from backend that will check this + // Small check if passwords are equal + if (signup_data.password1 !== signup_data.password2) { + alert("Passwords do not match"); + return; + } - try { - // Request without axios because no authentication is needed for this POST request - const response = await fetch(host, { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(signup_data), - }); - if (response.status == 201) { - alert("Successfully created account"); - await router.push("/login"); - } - } catch (error) { - console.error(error); + try { + // Request without axios because no authentication is needed for this POST request + const response = await fetch(host, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signup_data), + }); + if (response.status == 201) { + alert("Successfully created account"); + await router.push("/login"); } -} + } catch (error) { + console.error(error); + } +}; export default signup; diff --git a/frontend/next.config.js b/frontend/next.config.js index 03ab1813..b47f2f19 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -4,12 +4,12 @@ const nextConfig = { async redirects() { return [ { - source: '/', - destination: '/login', + source: "/", + destination: "/login", permanent: true, }, - ] - } -} + ]; + }, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index bf6c505c..5cfa7765 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,12 +1,11 @@ -import '@/styles/globals.css' -import type {AppProps} from 'next/app' -import {AuthProvider} from "@/context/AuthProvider"; +import "@/styles/globals.css"; +import type { AppProps } from "next/app"; +import { AuthProvider } from "@/context/AuthProvider"; -export default function App({Component, pageProps}: AppProps) { - return ( - - - - - ); +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); } diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx index 81f4832d..b2fff8b4 100644 --- a/frontend/pages/_document.tsx +++ b/frontend/pages/_document.tsx @@ -1,13 +1,13 @@ -import {Html, Head, Main, NextScript} from 'next/document' +import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { - return ( - - - -
- - - - ) + return ( + + + +
+ + + + ); } diff --git a/frontend/pages/api/axios.tsx b/frontend/pages/api/axios.tsx index 10907bfb..5a97ee09 100644 --- a/frontend/pages/api/axios.tsx +++ b/frontend/pages/api/axios.tsx @@ -1,44 +1,44 @@ -import axios from 'axios'; -import {useRouter} from "next/router"; +import axios from "axios"; +import { useRouter } from "next/router"; // Instance used to make authenticated requests const api = axios.create({ - baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, - withCredentials: true + baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, + withCredentials: true, }); // Intercept on request and add access tokens to request api.interceptors.request.use( - (config) => { - return config; - }, - (error) => { - return Promise.reject(error); - } + (config) => { + return config; + }, + (error) => { + return Promise.reject(error); + } ); // Intercept on response and renew refresh token if necessary api.interceptors.response.use( - (response) => { - return response; - }, - async (error) => { - console.error(error.response); - if (error.response.status === 401 && !error.config._retry) { - error.config._retry = true; - try { - const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}` - await api.post(request_url); - return api.request(error.config); - } catch (error) { - console.error(error); - const router = useRouter(); - await router.push('/login'); - throw error; - } - } - return Promise.reject(error); + (response) => { + return response; + }, + async (error) => { + console.error(error.response); + if (error.response.status === 401 && !error.config._retry) { + error.config._retry = true; + try { + const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}`; + await api.post(request_url); + return api.request(error.config); + } catch (error) { + console.error(error); + const router = useRouter(); + await router.push("/login"); + throw error; + } } + return Promise.reject(error); + } ); -export default api; \ No newline at end of file +export default api; diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 3c491713..89694c49 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,6 +1,5 @@ function Home() { - return null; // instant redirect to login page + return null; // instant redirect to login page } export default Home; - diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index b5ed433d..04a1d9c0 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -1,61 +1,83 @@ import BaseHeader from "@/components/header/BaseHeader"; -import styles from "styles/Login.module.css" +import styles from "styles/Login.module.css"; import Image from "next/image"; -import filler_logo from "../public/filler_logo.png" +import filler_logo from "../public/filler_logo.png"; import Link from "next/link"; -import login from "../lib/login" -import {FormEvent, useContext, useState} from "react"; -import {useRouter} from "next/router"; +import login from "../lib/login"; +import { FormEvent, useContext, useState } from "react"; +import { useRouter } from "next/router"; import AuthContext from "@/context/AuthProvider"; export default function Login() { - let {loginUser} = useContext(AuthContext); + let { loginUser } = useContext(AuthContext); - const router = useRouter(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); + const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); - const handleSubmit = async (event: FormEvent): Promise => { - event.preventDefault(); - try { - await login(username, password, router, loginUser); - } catch (error) { - console.error(error); - } - }; + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + try { + await login(username, password, router, loginUser); + } catch (error) { + console.error(error); + } + }; - return ( - <> - -
-
- My App Logo -
-
-

Login.

-
- - ) => setUsername(e.target.value)} - /> - - ) => setPassword(e.target.value)} - required - /> - -
-

Forgot Password

-

Don't have an account? Sign up here -

-
-
- - ) -} \ No newline at end of file + return ( + <> + +
+
+ My App Logo +
+
+

Login.

+
+ + ) => + setUsername(e.target.value) + } + /> + + ) => + setPassword(e.target.value) + } + required + /> + +
+

+ + Forgot Password + +

+

+ Don't have an account?{" "} + + Sign up here + +

+
+
+ + ); +} diff --git a/frontend/pages/reset-password.tsx b/frontend/pages/reset-password.tsx index c41d9159..8554d413 100644 --- a/frontend/pages/reset-password.tsx +++ b/frontend/pages/reset-password.tsx @@ -1,5 +1,5 @@ -import {useRouter} from "next/router"; -import React, {FormEvent, useState} from "react"; +import { useRouter } from "next/router"; +import React, { FormEvent, useState } from "react"; import BaseHeader from "@/components/header/BaseHeader"; import styles from "@/styles/Login.module.css"; import Image from "next/image"; @@ -8,45 +8,62 @@ import Link from "next/link"; import reset from "@/lib/reset"; export default function ResetPassword() { - const router = useRouter(); - const [email, setEmail] = useState(""); + const router = useRouter(); + const [email, setEmail] = useState(""); - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - try { - await reset(email, router); - } catch (error) { - console.error(error); - } + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + await reset(email, router); + } catch (error) { + console.error(error); } + }; - return ( - <> - -
-
- My App Logo -
-
-

Enter your e-mail in order to find your account

-
-
- - ) => setEmail(e.target.value)} - required/> + return ( + <> + +
+
+ My App Logo +
+
+

+ Enter your e-mail in order to find your account +

+
+ + + ) => + setEmail(e.target.value) + } + required + /> - - -

Already have an account? Log in here -

-
-
- - ); -} \ No newline at end of file + + +

+ Already have an account?{" "} + + Log in here + +

+
+
+ + ); +} diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 35b95217..4e3599d8 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -1,5 +1,5 @@ -import {useRouter} from "next/router"; -import React, {FormEvent, useState} from "react"; +import { useRouter } from "next/router"; +import React, { FormEvent, useState } from "react"; import BaseHeader from "@/components/header/BaseHeader"; import styles from "@/styles/Login.module.css"; import Image from "next/image"; @@ -7,91 +7,124 @@ import filler_logo from "@/public/filler_logo.png"; import Link from "next/link"; import signup from "@/lib/signup"; - export default function Signup() { - const router = useRouter(); - const [firstname, setFirstname] = useState(""); - const [lastname, setLastname] = useState(""); - const [email, setEmail] = useState(""); - const [password1, setPassword1] = useState(""); - const [password2, setPassword2] = useState(""); - + const router = useRouter(); + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); + const [email, setEmail] = useState(""); + const [password1, setPassword1] = useState(""); + const [password2, setPassword2] = useState(""); - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - try { - await signup(firstname, lastname, email, password1, password2, router); - } catch (error) { - console.error(error); - } + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + await signup(firstname, lastname, email, password1, password2, router); + } catch (error) { + console.error(error); } + }; - return ( - <> - -
-
- My App Logo -
-
-

Signup

-
- - ) => setFirstname(e.target.value)} - required/> + return ( + <> + +
+
+ My App Logo +
+
+

Signup

+ + + ) => + setFirstname(e.target.value) + } + required + /> - - ) => setLastname(e.target.value)} - required/> + + ) => + setLastname(e.target.value) + } + required + /> - - ) => setEmail(e.target.value)} - required/> + + ) => + setEmail(e.target.value) + } + required + /> - - ) => setPassword1(e.target.value)} - required/> + + ) => + setPassword1(e.target.value) + } + required + /> - - ) => setPassword2(e.target.value)} - required/> + + ) => + setPassword2(e.target.value) + } + required + /> - - -

Already have an account? Log in here -

-
-
- - ); -} \ No newline at end of file + + +

+ Already have an account?{" "} + + Log in here + +

+
+
+ + ); +} diff --git a/frontend/pages/welcome.tsx b/frontend/pages/welcome.tsx index 0d2704a4..9af7cd92 100644 --- a/frontend/pages/welcome.tsx +++ b/frontend/pages/welcome.tsx @@ -2,76 +2,80 @@ import BaseHeader from "@/components/header/BaseHeader"; import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; -import api from "../pages/api/axios" -import {useContext, useEffect, useState} from "react"; -import {useRouter} from "next/router"; +import api from "../pages/api/axios"; +import { useContext, useEffect, useState } from "react"; +import { useRouter } from "next/router"; import AuthContext from "@/context/AuthProvider"; function Welcome() { - let {auth, logoutUser} = useContext(AuthContext); - const router = useRouter(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); // prevents preview welcome page before auth check + let { auth, logoutUser } = useContext(AuthContext); + const router = useRouter(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); // prevents preview welcome page before auth check - useEffect(() => { - let present = "auth" in sessionStorage; - let value = sessionStorage.getItem('auth') - if (present && value == "true") { - setLoading(false); - fetchData(); - } else { - router.push('/login'); - } - }, []); + useEffect(() => { + let present = "auth" in sessionStorage; + let value = sessionStorage.getItem("auth"); + if (present && value == "true") { + setLoading(false); + fetchData(); + } else { + router.push("/login"); + } + }, []); - async function fetchData() { - try { - api.get(`${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}`).then(info => { - if (!info.data || info.data.length === 0) { - router.push('/login'); - } else { - setData(info.data); - } - }); - } catch (error) { - console.error(error); - } + async function fetchData() { + try { + api + .get( + `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}` + ) + .then((info) => { + if (!info.data || info.data.length === 0) { + router.push("/login"); + } else { + setData(info.data); + } + }); + } catch (error) { + console.error(error); } + } - const handleLogout = async () => { - try { - const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); - if (response.status === 200) { - logoutUser(); - await router.push('/login'); - } - } catch (error) { - console.error(error); - } - }; + const handleLogout = async () => { + try { + const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); + if (response.status === 200) { + logoutUser(); + await router.push("/login"); + } + } catch (error) { + console.error(error); + } + }; - return ( + return ( + <> + {loading ? ( +
Loading...
+ ) : ( <> - {loading ? ( -
Loading...
- ) : ( - <> - -

Welcome!

- Site coming soon - -

Users:

-
    - {data.map((item, index) => ( -
  • {JSON.stringify(item)}
  • - ))} -
- - )} + +

Welcome!

+ Site coming soon + +

Users:

+
    + {data.map((item, index) => ( +
  • {JSON.stringify(item)}
  • + ))} +
- ) - - + )} + + ); } -export default Welcome \ No newline at end of file +export default Welcome; diff --git a/frontend/types.d.tsx b/frontend/types.d.tsx index d98e2945..32968a26 100644 --- a/frontend/types.d.tsx +++ b/frontend/types.d.tsx @@ -1,16 +1,16 @@ export type Login = { - email: string - password: string -} + email: string; + password: string; +}; export type SignUp = { - first_name: string, - last_name: string, - email: string, - password1: string, - password2: string, -} + first_name: string; + last_name: string; + email: string; + password1: string; + password2: string; +}; export type Reset_Password = { - email: string -} + email: string; +}; From 201d758cf93732c9ad05ea975f93dc67d2a250c5 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 16:24:39 +0100 Subject: [PATCH 0094/1000] #18 cleanup + changed to work on pull requests to main and develop --- .eslintrc.json | 24 ------------------------ .github/workflows/formatting.yml | 8 ++++---- formatting_test_file.js | 28 ---------------------------- formatting_test_file.py | 20 -------------------- 4 files changed, 4 insertions(+), 76 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 formatting_test_file.js delete mode 100644 formatting_test_file.py diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 30887016..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "env": { - "browser": true, - "node": true, - "es6": true - }, - "extends": [ - "eslint:recommended" - // "plugin:react/recommended" - ], - "parserOptions": { - "ecmaVersion": 2022, - "sourceType": "module" - }, - //"plugins": [ - // "react", - //"prettier" - //], - "rules": { - //"prettier/prettier": "error", - "no-console": "off", - "no-unused-vars": "warn" - } -} \ No newline at end of file diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 9ecff0ed..a6cdd31d 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,10 +1,10 @@ name: Format Code Base on: - push: - branches: [ '**' ] -# - main -# - develop + pull_request: + branches: + - main + - develop jobs: format: diff --git a/formatting_test_file.js b/formatting_test_file.js deleted file mode 100644 index ff92c548..00000000 --- a/formatting_test_file.js +++ /dev/null @@ -1,28 +0,0 @@ -// program to find the factorial of a number - -// take input from the user - -const number = 5; - -// checking if number is negative -if (number < 0) { - console.log( - "Error! " + "Factorial" + " for negative " + "number does not exist." - ); -} - -// if number is 0 -else if (number === 0) { - console.log(`The factorial - of ${number} is 1.`); -} - -// if number is positive -else { - let fact = 1; - for (let i = 1; i <= number; i++) { - fact *= i; - } - - console.log(`The factorial of ${number} is ${fact}.`); -} diff --git a/formatting_test_file.py b/formatting_test_file.py deleted file mode 100644 index ba5f08b7..00000000 --- a/formatting_test_file.py +++ /dev/null @@ -1,20 +0,0 @@ -j = [1, 2, 3] - -if 1 == 1 and 2 == 2: - pass - - -def foo(): - print("All " "the newlines above me should be deleted!") - - -if True: - print("No newline above me!") - - print("There is a newline above " "" "me, and that's OK!") - - -class Point: - x: int - - y: int From 263cb91f60c1ebeb0257c30dc941e98f56b94aee Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 16:34:07 +0100 Subject: [PATCH 0095/1000] #18 changed line length to 120, as requested by Tibo --- .github/workflows/formatting.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index a6cdd31d..6532ad85 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -23,13 +23,13 @@ jobs: uses: rickstaa/action-black@v1.3.1 id: action_black with: - black_args: "." + black_args: ". --line-length 120" # using `prettier` for JavaScript - name: Format JavaScript code uses: creyD/prettier_action@v4.3 with: - prettier_options: --write **/*.{js,tsx} + prettier_options: --print-width 120 --write **/*.{js,tsx} commit_message: "Auto formatted code" only_changed: true github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 07325c85bb0d4cacab0c61bca63600e6013a26ec Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 16:56:05 +0100 Subject: [PATCH 0096/1000] #18 test long lines --- .github/workflows/formatting.yml | 8 ++++---- long_line_test.js | 7 +++++++ long_line_test.py | 5 +++++ 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 long_line_test.js create mode 100644 long_line_test.py diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 6532ad85..06432d57 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,10 +1,10 @@ name: Format Code Base on: - pull_request: - branches: - - main - - develop + push: + branches: ["**"] +# - main +# - develop jobs: format: diff --git a/long_line_test.js b/long_line_test.js new file mode 100644 index 00000000..167c7fdc --- /dev/null +++ b/long_line_test.js @@ -0,0 +1,7 @@ +const function_with_long_body_and_lots_of_parameters = function(first, second, third, fourth, fifth, sixth, seventh, eighth, nineth, tenth) { + if (first + first === second && third + third === fourth && fifth + fifth === sixth && seventh + seventh === eighth && nineth + nineth === tenth) { + console.log("Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD"); + } +} + +function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) \ No newline at end of file diff --git a/long_line_test.py b/long_line_test.py new file mode 100644 index 00000000..88e3660a --- /dev/null +++ b/long_line_test.py @@ -0,0 +1,5 @@ +def function_with_long_body_and_lots_of_parameters(first, second, third, fourth, fifth, sixth, seventh, eighth, nineth, tenth): + if first + first == second and third + third == fourth and fifth + fifth == sixth and seventh + seventh == eighth and nineth + nineth == tenth: + print("Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD") + +function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) \ No newline at end of file From 5f451ecc94c4a90e8bedef592e8408a66d6bf005 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 15:56:41 +0000 Subject: [PATCH 0097/1000] Auto formatted code --- long_line_test.js | 31 +++++++++++++++++++++++++------ long_line_test.py | 19 +++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/long_line_test.js b/long_line_test.js index 167c7fdc..2e985c48 100644 --- a/long_line_test.js +++ b/long_line_test.js @@ -1,7 +1,26 @@ -const function_with_long_body_and_lots_of_parameters = function(first, second, third, fourth, fifth, sixth, seventh, eighth, nineth, tenth) { - if (first + first === second && third + third === fourth && fifth + fifth === sixth && seventh + seventh === eighth && nineth + nineth === tenth) { - console.log("Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD"); - } -} +const function_with_long_body_and_lots_of_parameters = function ( + first, + second, + third, + fourth, + fifth, + sixth, + seventh, + eighth, + nineth, + tenth +) { + if ( + first + first === second && + third + third === fourth && + fifth + fifth === sixth && + seventh + seventh === eighth && + nineth + nineth === tenth + ) { + console.log( + "Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD" + ); + } +}; -function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) \ No newline at end of file +function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512); diff --git a/long_line_test.py b/long_line_test.py index 88e3660a..e869d83f 100644 --- a/long_line_test.py +++ b/long_line_test.py @@ -1,5 +1,16 @@ -def function_with_long_body_and_lots_of_parameters(first, second, third, fourth, fifth, sixth, seventh, eighth, nineth, tenth): - if first + first == second and third + third == fourth and fifth + fifth == sixth and seventh + seventh == eighth and nineth + nineth == tenth: - print("Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD") +def function_with_long_body_and_lots_of_parameters( + first, second, third, fourth, fifth, sixth, seventh, eighth, nineth, tenth +): + if ( + first + first == second + and third + third == fourth + and fifth + fifth == sixth + and seventh + seventh == eighth + and nineth + nineth == tenth + ): + print( + "Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD" + ) -function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) \ No newline at end of file + +function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) From a64a4e3d986a6e12fcb96ee3e00062ae1a200464 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sat, 18 Mar 2023 17:01:56 +0100 Subject: [PATCH 0098/1000] #18 final cleanup --- .github/workflows/formatting.yml | 8 ++++---- long_line_test.js | 26 -------------------------- long_line_test.py | 16 ---------------- 3 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 long_line_test.js delete mode 100644 long_line_test.py diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 06432d57..6532ad85 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,10 +1,10 @@ name: Format Code Base on: - push: - branches: ["**"] -# - main -# - develop + pull_request: + branches: + - main + - develop jobs: format: diff --git a/long_line_test.js b/long_line_test.js deleted file mode 100644 index 2e985c48..00000000 --- a/long_line_test.js +++ /dev/null @@ -1,26 +0,0 @@ -const function_with_long_body_and_lots_of_parameters = function ( - first, - second, - third, - fourth, - fifth, - sixth, - seventh, - eighth, - nineth, - tenth -) { - if ( - first + first === second && - third + third === fourth && - fifth + fifth === sixth && - seventh + seventh === eighth && - nineth + nineth === tenth - ) { - console.log( - "Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD" - ); - } -}; - -function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512); diff --git a/long_line_test.py b/long_line_test.py deleted file mode 100644 index e869d83f..00000000 --- a/long_line_test.py +++ /dev/null @@ -1,16 +0,0 @@ -def function_with_long_body_and_lots_of_parameters( - first, second, third, fourth, fifth, sixth, seventh, eighth, nineth, tenth -): - if ( - first + first == second - and third + third == fourth - and fifth + fifth == sixth - and seventh + seventh == eighth - and nineth + nineth == tenth - ): - print( - "Hooray, this line is wayyyyyyyyy tooooooo long, so let's see if you can fix this. I believe in you :DDDDDDDDDDDD" - ) - - -function_with_long_body_and_lots_of_parameters(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) From 852e5a36bf0d6658c2940e5b531d28eafcb31687 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Sun, 19 Mar 2023 11:38:15 +0100 Subject: [PATCH 0099/1000] #18 changed JavaScript indent size to 4 instead of default 2 --- .github/workflows/formatting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 6532ad85..49b8e571 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -29,7 +29,7 @@ jobs: - name: Format JavaScript code uses: creyD/prettier_action@v4.3 with: - prettier_options: --print-width 120 --write **/*.{js,tsx} + prettier_options: --print-width 120 --tab-width 4 --write **/*.{js,tsx} commit_message: "Auto formatted code" only_changed: true github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 963956bf0c645a4fdd495afa6203f4c1e6c7e3d0 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Mon, 20 Mar 2023 09:34:05 +0100 Subject: [PATCH 0100/1000] region tests all functions --- backend/region/tests.py | 61 ++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/backend/region/tests.py b/backend/region/tests.py index 6b9e14c3..8501b2fd 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -43,29 +43,70 @@ def test_insert_1_region(self): # er moet ook een id bij zitten assert "id" in response.data - def test_insert_multiple_regions(self): + def test_get_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) data1 = { "region": "Gent" } + response1 = client.post("http://localhost:2002/region/", data1, follow=True) + assert response1.status_code == 201 + for key in data1: + # alle info zou er in moeten zitten + assert key in response1.data + # er moet ook een id bij zitten + assert "id" in response1.data + id = response1.data["id"] + response2 = client.get(f"http://localhost:2002/region/{id}/", follow=True) + assert response2.status_code == 200 + assert response2.data["region"] == "Gent" + assert "id" in response2.data + + + def test_patch_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data1 = { + "region": "Brugge" + } data2 = { - "region": "Antwerpen" + "region": "Gent" } response1 = client.post("http://localhost:2002/region/", data1, follow=True) - response2 = client.post("http://localhost:2002/region/", data2, follow=True) assert response1.status_code == 201 - assert response2.status_code == 201 for key in data1: # alle info zou er in moeten zitten assert key in response1.data - for key in data2: + # er moet ook een id bij zitten + assert "id" in response1.data + id = response1.data["id"] + response2 = client.patch(f"http://localhost:2002/region/{id}/", data2, follow=True) + assert response2.status_code == 200 + response3 = client.get(f"http://localhost:2002/region/{id}/", follow=True) + assert response3.status_code == 200 + assert response3.data["region"] == "Gent" + assert "id" in response3.data + + def test_remove_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data1 = { + "region": "Gent" + } + response1 = client.post("http://localhost:2002/region/", data1, follow=True) + assert response1.status_code == 201 + for key in data1: # alle info zou er in moeten zitten - assert key in response2.data + assert key in response1.data # er moet ook een id bij zitten assert "id" in response1.data - assert "id" in response2.data - results = client.get("/region/all/") - print([r for r in results.data]) - assert False + id = response1.data["id"] + response2 = client.delete(f"http://localhost:2002/region/{id}/", follow=True) + assert response2.status_code == 204 + response3 = client.get(f"http://localhost:2002/region/{id}/", follow=True) + # should be 404 I think + # assert response3.status_code == 404 + assert response3.status_code == 400 From 9d740aefee28d34c315eea6406e9084682c6a2eb Mon Sep 17 00:00:00 2001 From: sevrijss Date: Mon, 20 Mar 2023 10:18:52 +0100 Subject: [PATCH 0101/1000] fix localhost db for testing --- backend/config/settings.py | 10 +++++++--- backend/region/tests.py | 22 ++++++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/backend/config/settings.py b/backend/config/settings.py index 4650901a..24316782 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -10,8 +10,10 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ import collections +import sys from datetime import timedelta from pathlib import Path + from .secrets import DJANGO_SECRET_KEY, SECRET_EMAIL_USER, SECRET_EMAIL_USER_PSWD # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -79,7 +81,7 @@ # Use nose to run all tests TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -#drf-spectacular settings +# drf-spectacular settings SPECTACULAR_SETTINGS = { 'TITLE': 'Dr-Trottoir API', 'DESCRIPTION': 'This is the documentation for the Dr-trottoir API', @@ -166,8 +168,10 @@ 'NAME': 'drtrottoir', 'USER': 'django', 'PASSWORD': 'password', - 'HOST': 'localhost', # If you want to run using python manage.py runserver - # 'HOST': 'web', # If you want to use `docker-compose up` + # since testing is run outside the docker, we need a localhost db + # the postgres docker port is exposed to it should be used as well + # this 'hack' is just to fix the name resolving of 'web' + 'HOST': 'localhost' if "test" in sys.argv else "web", 'PORT': '5432', } } diff --git a/backend/region/tests.py b/backend/region/tests.py index 8501b2fd..0f7e640a 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -3,6 +3,8 @@ from base.models import User +backend_url = "http://localhost:2002" + def createUser(is_staff: bool = True) -> User: user = User( @@ -23,7 +25,7 @@ def test_empty_region_list(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - response = client.get("http://localhost:2002/region/all", follow=True) + response = client.get(f"{backend_url}/region/all", follow=True) assert response.status_code == 200 data = [response.data[e] for e in response.data] assert len(data) == 0 @@ -35,7 +37,7 @@ def test_insert_1_region(self): data = { "region": "Gent" } - response = client.post("http://localhost:2002/region/", data, follow=True) + response = client.post(f"{backend_url}/region/", data, follow=True) assert response.status_code == 201 for key in data: # alle info zou er in moeten zitten @@ -50,7 +52,7 @@ def test_get_region(self): data1 = { "region": "Gent" } - response1 = client.post("http://localhost:2002/region/", data1, follow=True) + response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 201 for key in data1: # alle info zou er in moeten zitten @@ -58,7 +60,7 @@ def test_get_region(self): # er moet ook een id bij zitten assert "id" in response1.data id = response1.data["id"] - response2 = client.get(f"http://localhost:2002/region/{id}/", follow=True) + response2 = client.get(f"{backend_url}/region/{id}/", follow=True) assert response2.status_code == 200 assert response2.data["region"] == "Gent" assert "id" in response2.data @@ -74,7 +76,7 @@ def test_patch_region(self): data2 = { "region": "Gent" } - response1 = client.post("http://localhost:2002/region/", data1, follow=True) + response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 201 for key in data1: # alle info zou er in moeten zitten @@ -82,9 +84,9 @@ def test_patch_region(self): # er moet ook een id bij zitten assert "id" in response1.data id = response1.data["id"] - response2 = client.patch(f"http://localhost:2002/region/{id}/", data2, follow=True) + response2 = client.patch(f"{backend_url}/region/{id}/", data2, follow=True) assert response2.status_code == 200 - response3 = client.get(f"http://localhost:2002/region/{id}/", follow=True) + response3 = client.get(f"{backend_url}/region/{id}/", follow=True) assert response3.status_code == 200 assert response3.data["region"] == "Gent" assert "id" in response3.data @@ -96,7 +98,7 @@ def test_remove_region(self): data1 = { "region": "Gent" } - response1 = client.post("http://localhost:2002/region/", data1, follow=True) + response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 201 for key in data1: # alle info zou er in moeten zitten @@ -104,9 +106,9 @@ def test_remove_region(self): # er moet ook een id bij zitten assert "id" in response1.data id = response1.data["id"] - response2 = client.delete(f"http://localhost:2002/region/{id}/", follow=True) + response2 = client.delete(f"{backend_url}/region/{id}/", follow=True) assert response2.status_code == 204 - response3 = client.get(f"http://localhost:2002/region/{id}/", follow=True) + response3 = client.get(f"{backend_url}/region/{id}/", follow=True) # should be 404 I think # assert response3.status_code == 404 assert response3.status_code == 400 From ee3f5e1e3bd95f8eab659d22388cb2faef50f595 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Mon, 20 Mar 2023 11:54:45 +0100 Subject: [PATCH 0102/1000] test error handling --- backend/region/tests.py | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/backend/region/tests.py b/backend/region/tests.py index 0f7e640a..12c7b3c6 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -30,7 +30,7 @@ def test_empty_region_list(self): data = [response.data[e] for e in response.data] assert len(data) == 0 - def test_insert_1_region(self): + def test_insert_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) @@ -45,6 +45,17 @@ def test_insert_1_region(self): # er moet ook een id bij zitten assert "id" in response.data + def test_insert_dupe_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data = { + "region": "Gent" + } + _ = client.post(f"{backend_url}/region/", data, follow=True) + response = client.post(f"{backend_url}/region/", data, follow=True) + assert response.status_code == 400 + def test_get_region(self): user = createUser() client = APIClient() @@ -65,6 +76,14 @@ def test_get_region(self): assert response2.data["region"] == "Gent" assert "id" in response2.data + def test_get_non_existing(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + response1 = client.get(f"{backend_url}/region/123654897", follow=True) + # should be 404 I think + # assert response1.status_code == 404 + assert response1.status_code == 400 def test_patch_region(self): user = createUser() @@ -91,6 +110,23 @@ def test_patch_region(self): assert response3.data["region"] == "Gent" assert "id" in response3.data + def test_patch_error_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data1 = { + "region": "Brugge" + } + data2 = { + "region": "Gent" + } + response1 = client.post(f"{backend_url}/region/", data1, follow=True) + _ = client.post(f"{backend_url}/region/", data2, follow=True) + assert response1.status_code == 201 + id = response1.data["id"] + response2 = client.patch(f"{backend_url}/region/{id}/", data2, follow=True) + assert response2.status_code == 400 + def test_remove_region(self): user = createUser() client = APIClient() @@ -112,3 +148,21 @@ def test_remove_region(self): # should be 404 I think # assert response3.status_code == 404 assert response3.status_code == 400 + + def test_remove_non_existent_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + response2 = client.delete(f"{backend_url}/region/123456789/", follow=True) + assert response2.status_code == 400 + + def test_add_existing_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data1 = { + "region": "Gent" + } + _ = client.post(f"{backend_url}/region/", data1, follow=True) + response1 = client.post(f"{backend_url}/region/", data1, follow=True) + assert response1.status_code == 400 From 62a5eecbe4205b7ae7f2dcef849db272a4181195 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Mon, 20 Mar 2023 17:28:43 +0000 Subject: [PATCH 0103/1000] Auto formatted code --- backend/config/urls.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/config/urls.py b/backend/config/urls.py index 98a6bca3..da8de962 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -35,21 +35,21 @@ from .settings import MEDIA_URL, MEDIA_ROOT urlpatterns = [ - path("admin/", admin.site.urls), - path("docs/", SpectacularAPIView.as_view(), name="schema"), - path("docs/ui/", SpectacularSwaggerView.as_view(url="/api/docs"), name="swagger-ui"), - path("authentication/", include(authentication_urls)), - path("manual/", include(manual_urls)), - path("picture_building/", include(picture_building_urls)), - path("building/", include(building_urls)), - path("building_comment/", include(building_comment_urls)), - path("region/", include(region_urls)), - path("buildingurl/", include(building_url_urls)), - path("garbage_collection/", include(garbage_collection_urls)), - path("building_on_tour/", include(building_on_tour_urls)), - path("user/", include(user_urls)), - path("role/", include(role_urls)), - path("student_at_building_on_tour/", include(stud_buil_tour_urls)), - path("tour/", include(tour_urls)), - re_path(r"^$", RedirectView.as_view(url=reverse_lazy("api"), permanent=False)), - ] + static(MEDIA_URL, document_root=MEDIA_ROOT) + path("admin/", admin.site.urls), + path("docs/", SpectacularAPIView.as_view(), name="schema"), + path("docs/ui/", SpectacularSwaggerView.as_view(url="/api/docs"), name="swagger-ui"), + path("authentication/", include(authentication_urls)), + path("manual/", include(manual_urls)), + path("picture_building/", include(picture_building_urls)), + path("building/", include(building_urls)), + path("building_comment/", include(building_comment_urls)), + path("region/", include(region_urls)), + path("buildingurl/", include(building_url_urls)), + path("garbage_collection/", include(garbage_collection_urls)), + path("building_on_tour/", include(building_on_tour_urls)), + path("user/", include(user_urls)), + path("role/", include(role_urls)), + path("student_at_building_on_tour/", include(stud_buil_tour_urls)), + path("tour/", include(tour_urls)), + re_path(r"^$", RedirectView.as_view(url=reverse_lazy("api"), permanent=False)), +] + static(MEDIA_URL, document_root=MEDIA_ROOT) From cb91225825936aa9ad293bd21cb1a345f66c1d2f Mon Sep 17 00:00:00 2001 From: sevrijss Date: Mon, 20 Mar 2023 19:44:51 +0100 Subject: [PATCH 0104/1000] final error handling test --- backend/region/tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/region/tests.py b/backend/region/tests.py index 12c7b3c6..285fafae 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -110,6 +110,14 @@ def test_patch_region(self): assert response3.data["region"] == "Gent" assert "id" in response3.data + def test_patch_invalid_region(self): + user = createUser() + client = APIClient() + client.force_authenticate(user=user) + data = rf.getRegion() + response2 = client.patch(f"{backend_url}/region/123434687658/", data, follow=True) + assert response2.status_code == 400 + def test_patch_error_region(self): user = createUser() client = APIClient() From ccba4ef61ebc5da5c6e46ba7b5a06eb0bbdfdb72 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Mon, 20 Mar 2023 19:45:02 +0100 Subject: [PATCH 0105/1000] use region factory --- backend/base/factories.py | 25 ++++++++++++++++++++++ backend/region/tests.py | 45 +++++++++++---------------------------- backend/region/views.py | 1 + backend/requirements.txt | 3 ++- 4 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 backend/base/factories.py diff --git a/backend/base/factories.py b/backend/base/factories.py new file mode 100644 index 00000000..2b5c687c --- /dev/null +++ b/backend/base/factories.py @@ -0,0 +1,25 @@ +import factory +import factory.fuzzy as ff + +from .models import Region +from .serializers import RegionSerializer + + +class RegionFactory(factory.Factory): + region = ff.FuzzyChoice(choices=[ + "Gent", + "Brugge", + "Antwerpen" + ]) + + class Meta: + model = Region + + @staticmethod + def getRegion(**kwargs): + instance = RegionFactory.create(**kwargs) + serializer = RegionSerializer(instance) + data = serializer.data + # we are not interested in the id + del data["id"] + return dict(data) diff --git a/backend/region/tests.py b/backend/region/tests.py index 285fafae..a4334d88 100644 --- a/backend/region/tests.py +++ b/backend/region/tests.py @@ -1,6 +1,7 @@ from django.test import TestCase from rest_framework.test import APIClient +from base.factories import RegionFactory from base.models import User backend_url = "http://localhost:2002" @@ -20,6 +21,9 @@ def createUser(is_staff: bool = True) -> User: return user +rf = RegionFactory + + class RegionTests(TestCase): def test_empty_region_list(self): user = createUser() @@ -34,9 +38,7 @@ def test_insert_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data = { - "region": "Gent" - } + data = rf.getRegion() response = client.post(f"{backend_url}/region/", data, follow=True) assert response.status_code == 201 for key in data: @@ -49,9 +51,7 @@ def test_insert_dupe_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data = { - "region": "Gent" - } + data = rf.getRegion() _ = client.post(f"{backend_url}/region/", data, follow=True) response = client.post(f"{backend_url}/region/", data, follow=True) assert response.status_code == 400 @@ -60,9 +60,7 @@ def test_get_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data1 = { - "region": "Gent" - } + data1 = rf.getRegion() response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 201 for key in data1: @@ -89,12 +87,8 @@ def test_patch_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data1 = { - "region": "Brugge" - } - data2 = { - "region": "Gent" - } + data1 = rf.getRegion(region="Brugge") + data2 = rf.getRegion(region="Gent") response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 201 for key in data1: @@ -122,12 +116,8 @@ def test_patch_error_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data1 = { - "region": "Brugge" - } - data2 = { - "region": "Gent" - } + data1 = rf.getRegion(region="Brugge") + data2 = rf.getRegion(region="Gent") response1 = client.post(f"{backend_url}/region/", data1, follow=True) _ = client.post(f"{backend_url}/region/", data2, follow=True) assert response1.status_code == 201 @@ -139,16 +129,9 @@ def test_remove_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data1 = { - "region": "Gent" - } + data1 = rf.getRegion() response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 201 - for key in data1: - # alle info zou er in moeten zitten - assert key in response1.data - # er moet ook een id bij zitten - assert "id" in response1.data id = response1.data["id"] response2 = client.delete(f"{backend_url}/region/{id}/", follow=True) assert response2.status_code == 204 @@ -168,9 +151,7 @@ def test_add_existing_region(self): user = createUser() client = APIClient() client.force_authenticate(user=user) - data1 = { - "region": "Gent" - } + data1 = rf.getRegion() _ = client.post(f"{backend_url}/region/", data1, follow=True) response1 = client.post(f"{backend_url}/region/", data1, follow=True) assert response1.status_code == 400 diff --git a/backend/region/views.py b/backend/region/views.py index 5f8bf9a3..badeaf1f 100644 --- a/backend/region/views.py +++ b/backend/region/views.py @@ -19,6 +19,7 @@ def post(self, request): if key in vars(region_instance): setattr(region_instance, key, data[key]) if r := try_full_clean_and_save(region_instance): + print(r.data) return r serializer = RegionSerializer(region_instance) diff --git a/backend/requirements.txt b/backend/requirements.txt index fba1dc31..5046cc2e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -30,4 +30,5 @@ urllib3==1.26.14 Pillow==9.4.0 drf-spectacular==0.26.0 django-nose==1.4.7 -coverage==7.2.1 \ No newline at end of file +coverage==7.2.1 +factory-boy==3.2.1 \ No newline at end of file From 21705eb3f8955b302151ccca2ae51c167913607d Mon Sep 17 00:00:00 2001 From: TiboStr Date: Mon, 20 Mar 2023 19:50:34 +0000 Subject: [PATCH 0106/1000] Auto formatted code --- ...te_emailwhitelist_building_bus_and_more.py | 54 ++++++++++++------- backend/base/models.py | 53 +++++++----------- backend/config/urls.py | 4 +- 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py index 3fcc732d..e47b3aa5 100644 --- a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py +++ b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py @@ -4,48 +4,64 @@ class Migration(migrations.Migration): - dependencies = [ - ('base', '0001_initial'), + ("base", "0001_initial"), ] operations = [ migrations.CreateModel( - name='EmailTemplate', + name="EmailTemplate", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40)), - ('template', models.TextField()), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=40)), + ("template", models.TextField()), ], ), migrations.CreateModel( - name='EmailWhitelist', + name="EmailWhitelist", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('email', models.EmailField(error_messages={'unique': 'This email is already on the whitelist.'}, max_length=254, unique=True, verbose_name='email address')), - ('verification_code', models.CharField(error_messages={'unique': 'This verification code already exists.'}, max_length=128, unique=True)), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "email", + models.EmailField( + error_messages={"unique": "This email is already on the whitelist."}, + max_length=254, + unique=True, + verbose_name="email address", + ), + ), + ( + "verification_code", + models.CharField( + error_messages={"unique": "This verification code already exists."}, max_length=128, unique=True + ), + ), ], ), migrations.AddField( - model_name='building', - name='bus', + model_name="building", + name="bus", field=models.CharField(blank=True, max_length=10, null=True), ), migrations.AddField( - model_name='building', - name='public_id', + model_name="building", + name="public_id", field=models.CharField(blank=True, max_length=32, null=True), ), migrations.AlterField( - model_name='building', - name='house_number', + model_name="building", + name="house_number", field=models.PositiveIntegerField(), ), migrations.DeleteModel( - name='BuildingURL', + name="BuildingURL", ), migrations.AddConstraint( - model_name='emailtemplate', - constraint=models.UniqueConstraint(models.F('name'), name='unique_template_name', violation_error_message='The name for this template already exists.'), + model_name="emailtemplate", + constraint=models.UniqueConstraint( + models.F("name"), + name="unique_template_name", + violation_error_message="The name for this template already exists.", + ), ), ] diff --git a/backend/base/models.py b/backend/base/models.py index 9fd11642..0c6504f8 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -20,15 +20,11 @@ def _check_for_present_keys(instance, keys_iterable): for key in keys_iterable: if not vars(instance)[key]: - raise ValidationError( - f"Tried to access {key}, but it was not found in object" - ) + raise ValidationError(f"Tried to access {key}, but it was not found in object") class Region(models.Model): - region = models.CharField( - max_length=40, unique=True, error_messages={"unique": "Deze regio bestaat al."} - ) + region = models.CharField(max_length=40, unique=True, error_messages={"unique": "Deze regio bestaat al."}) def __str__(self): return self.region @@ -54,9 +50,7 @@ def clean(self): if Role.objects.count() != 0 and self.rank != MAX_INT: highest_rank = Role.objects.order_by("-rank").first().rank if self.rank > highest_rank + 1: - raise ValidationError( - f"The maximum rank allowed is {highest_rank + 1}." - ) + raise ValidationError(f"The maximum rank allowed is {highest_rank + 1}.") class Meta: constraints = [ @@ -97,11 +91,13 @@ def __str__(self): class EmailWhitelist(models.Model): - email = models.EmailField(_('email address'), unique=True, - error_messages={'unique': "This email is already on the whitelist."}) + email = models.EmailField( + _("email address"), unique=True, error_messages={"unique": "This email is already on the whitelist."} + ) # The verification code, preferably hashed - verification_code = models.CharField(max_length=128, unique=True, - error_messages={'unique': "This verification code already exists."}) + verification_code = models.CharField( + max_length=128, unique=True, error_messages={"unique": "This verification code already exists."} + ) class Building(models.Model): @@ -131,8 +127,8 @@ def clean(self): raise ValidationError("The house number of the building must be positive and not zero.") user = self.syndic - if user.role.name.lower() != 'syndic': - raise ValidationError("Only a user with role \"syndic\" can own a building.") + if user.role.name.lower() != "syndic": + raise ValidationError('Only a user with role "syndic" can own a building.') class Meta: constraints = [ @@ -253,15 +249,12 @@ def clean(self): building_region = self.building.region if tour_region != building_region: raise ValidationError( - f"The regions for tour ({tour_region}) en building ({building_region}) " - f"are different." + f"The regions for tour ({tour_region}) en building ({building_region}) " f"are different." ) nr_of_buildings = BuildingOnTour.objects.filter(tour=self.tour).count() if self.index > nr_of_buildings: - raise ValidationError( - f"The maximum allowed index for this building is {nr_of_buildings}" - ) + raise ValidationError(f"The maximum allowed index for this building is {nr_of_buildings}") def __str__(self): return f"{self.building} on tour {self.tour}, index: {self.index}" @@ -284,9 +277,7 @@ class Meta: class StudentAtBuildingOnTour(models.Model): - building_on_tour = models.ForeignKey( - BuildingOnTour, on_delete=models.SET_NULL, null=True - ) + building_on_tour = models.ForeignKey(BuildingOnTour, on_delete=models.SET_NULL, null=True) date = models.DateField() student = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) @@ -302,11 +293,7 @@ def clean(self): if user.role.name.lower() == "syndic": raise ValidationError("A syndic can't do tours") building_on_tour_region = self.building_on_tour.tour.region - if ( - not self.student.region.all() - .filter(region=building_on_tour_region) - .exists() - ): + if not self.student.region.all().filter(region=building_on_tour_region).exists(): raise ValidationError( f"Student ({user.email}) doesn't do tours in this region ({building_on_tour_region})." ) @@ -348,9 +335,7 @@ class PictureBuilding(models.Model): def clean(self): super().clean() - _check_for_present_keys( - self, {"building_id", "picture", "description", "timestamp"} - ) + _check_for_present_keys(self, {"building_id", "picture", "description", "timestamp"}) class Meta: constraints = [ @@ -412,8 +397,8 @@ class EmailTemplate(models.Model): class Meta: constraints = [ UniqueConstraint( - 'name', - name='unique_template_name', - violation_error_message='The name for this template already exists.' + "name", + name="unique_template_name", + violation_error_message="The name for this template already exists.", ), ] diff --git a/backend/config/urls.py b/backend/config/urls.py index ba969041..2d185cee 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -36,9 +36,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("docs/", SpectacularAPIView.as_view(), name="schema"), - path( - "docs/ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" - ), + path("docs/ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), path("authentication/", include(authentication_urls)), path("manual/", include(manual_urls)), path("picture_building/", include(picture_building_urls)), From a9887ace13fed4b3c94a716f1ffd32be5c2fa455 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 21:05:31 +0100 Subject: [PATCH 0107/1000] Added new fields of building to its serializer #90 --- backend/base/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/base/serializers.py b/backend/base/serializers.py index e7f12d6a..ab64b823 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -35,11 +35,13 @@ class Meta: "postal_code", "street", "house_number", + "bus" "client_number", "duration", "syndic", "region", "name", + "public_id" ] read_only_fields = ["id"] From e27619d73f822274f5da93fa0e19ed518f5764ad Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 21:19:33 +0100 Subject: [PATCH 0108/1000] Created django apps for EmailTemplate and EmailWhitelist (#90) --- backend/email_template/__init__.py | 0 backend/email_template/apps.py | 6 ++++++ backend/email_template/models.py | 3 +++ backend/email_template/tests.py | 3 +++ backend/email_template/urls.py | 0 backend/email_template/views.py | 3 +++ backend/email_whitelist/__init__.py | 0 backend/email_whitelist/apps.py | 6 ++++++ backend/email_whitelist/tests.py | 3 +++ backend/email_whitelist/urls.py | 0 backend/email_whitelist/views.py | 3 +++ 11 files changed, 27 insertions(+) create mode 100644 backend/email_template/__init__.py create mode 100644 backend/email_template/apps.py create mode 100644 backend/email_template/models.py create mode 100644 backend/email_template/tests.py create mode 100644 backend/email_template/urls.py create mode 100644 backend/email_template/views.py create mode 100644 backend/email_whitelist/__init__.py create mode 100644 backend/email_whitelist/apps.py create mode 100644 backend/email_whitelist/tests.py create mode 100644 backend/email_whitelist/urls.py create mode 100644 backend/email_whitelist/views.py diff --git a/backend/email_template/__init__.py b/backend/email_template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/email_template/apps.py b/backend/email_template/apps.py new file mode 100644 index 00000000..468700ad --- /dev/null +++ b/backend/email_template/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EmailTemplateConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "email_template" diff --git a/backend/email_template/models.py b/backend/email_template/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/backend/email_template/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/email_template/tests.py b/backend/email_template/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backend/email_template/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/email_template/urls.py b/backend/email_template/urls.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/email_template/views.py b/backend/email_template/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/backend/email_template/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/email_whitelist/__init__.py b/backend/email_whitelist/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/email_whitelist/apps.py b/backend/email_whitelist/apps.py new file mode 100644 index 00000000..ede2c6e9 --- /dev/null +++ b/backend/email_whitelist/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EmailWhitelistConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "email_whitelist" diff --git a/backend/email_whitelist/tests.py b/backend/email_whitelist/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backend/email_whitelist/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/email_whitelist/urls.py b/backend/email_whitelist/urls.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/email_whitelist/views.py b/backend/email_whitelist/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/backend/email_whitelist/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From fe49068f27036f7b26a08c2dd793bad5314078bc Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 21:19:54 +0100 Subject: [PATCH 0109/1000] EmailTemplateSerializer (#90) --- backend/base/models.py | 13 ++++++------- backend/base/serializers.py | 7 +++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index 0c6504f8..7635a6ed 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -7,14 +7,13 @@ from django.db.models import UniqueConstraint from django.db.models.functions import Lower from django.utils.translation import gettext_lazy as _ -from django_random_id_model import RandomIDModel from phonenumber_field.modelfields import PhoneNumberField from users.managers import UserManager # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2**31 - 1 +MAX_INT = 2 ** 31 - 1 def _check_for_present_keys(instance, keys_iterable): @@ -92,7 +91,7 @@ def __str__(self): class EmailWhitelist(models.Model): email = models.EmailField( - _("email address"), unique=True, error_messages={"unique": "This email is already on the whitelist."} + _("email address"), unique=True, error_messages={"unique": "This email is already in the whitelist."} ) # The verification code, preferably hashed verification_code = models.CharField( @@ -199,7 +198,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -373,9 +372,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 diff --git a/backend/base/serializers.py b/backend/base/serializers.py index ab64b823..acbb148d 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -53,6 +53,13 @@ class Meta: read_only_fields = ["id"] +class EmailTemplateSerializer(serializers.ModelSerializer): + class Meta: + model = EmailTemplate + fields = ["id", "name", "template"] + read_only_fields = ["id"] + + class PictureBuildingSerializer(serializers.ModelSerializer): class Meta: model = PictureBuilding From daa6470bd768833ae56ddd992f0a01f678b83583 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 21:21:16 +0100 Subject: [PATCH 0110/1000] Made id read only field in all serializers --- backend/base/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/base/serializers.py b/backend/base/serializers.py index acbb148d..7d45316c 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -64,12 +64,14 @@ class PictureBuildingSerializer(serializers.ModelSerializer): class Meta: model = PictureBuilding fields = ["id", "building", "picture", "description", "timestamp", "type"] + read_only_fields = ["id"] class StudBuildTourSerializer(serializers.ModelSerializer): class Meta: model = StudentAtBuildingOnTour fields = ["id", "building_on_tour", "date", "student"] + read_only_fields = ["id"] class GarbageCollectionSerializer(serializers.ModelSerializer): @@ -83,21 +85,25 @@ class ManualSerializer(serializers.ModelSerializer): class Meta: model = Manual fields = ["id", "building", "version_number", "file"] + read_only_fields = ["id"] class BuildingTourSerializer(serializers.ModelSerializer): class Meta: model = BuildingOnTour fields = ["id", "building", "tour", "index"] + read_only_fields = ["id"] class TourSerializer(serializers.ModelSerializer): class Meta: model = Tour fields = ["id", "name", "region", "modified_at"] + read_only_fields = ["id"] class RegionSerializer(serializers.ModelSerializer): class Meta: model = Region fields = ["id", "region"] + read_only_fields = ["id"] From 11cd1cde3e24c526b32bd51af1ef8bebecf7c3c3 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 21:23:43 +0100 Subject: [PATCH 0111/1000] EmailWhitelistSerializer (#90) --- backend/base/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/base/serializers.py b/backend/base/serializers.py index 7d45316c..425f992a 100644 --- a/backend/base/serializers.py +++ b/backend/base/serializers.py @@ -60,6 +60,13 @@ class Meta: read_only_fields = ["id"] +class EmailWhitelistSerializer(serializers.ModelSerializer): + class Meta: + model = EmailWhitelist + fields = ["id", "email", "verification_code"] + read_only_fields = ["id"] + + class PictureBuildingSerializer(serializers.ModelSerializer): class Meta: model = PictureBuilding From a53ccfae6a815442956143492d9ca64548cc061f Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 21:33:16 +0100 Subject: [PATCH 0112/1000] bugfix can edit role permission --- backend/authorisation/permissions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 77d7518e..32d0e620 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -142,6 +142,7 @@ class CanEditRole(BasePermission): def has_object_permission(self, request, view, obj: User): if request.method in ['PATCH']: data = request_to_dict(request.data) - role_instance = Role.objects.filter(id=data['role'])[0] - return request.user.role.rank <= role_instance.rank + if 'role' in data.keys(): + role_instance = Role.objects.filter(id=data['role'])[0] + return request.user.role.rank <= role_instance.rank return True From d99305bd4000be8c6c921ee2863ce37a78597dab Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Mon, 20 Mar 2023 21:34:36 +0100 Subject: [PATCH 0113/1000] Changed unique constraint building --- ...2_emailtemplate_emailwhitelist_and_more.py | 64 ++++++++++++++++++ ...te_emailwhitelist_building_bus_and_more.py | 67 ------------------- backend/base/models.py | 3 +- 3 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py delete mode 100644 backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py new file mode 100644 index 00000000..cd18f20d --- /dev/null +++ b/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 4.1.7 on 2023-03-20 20:33 + +from django.db import migrations, models +import django.db.models.functions.text + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EmailTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('template', models.TextField()), + ], + ), + migrations.CreateModel( + name='EmailWhitelist', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(error_messages={'unique': 'This email is already on the whitelist.'}, max_length=254, unique=True, verbose_name='email address')), + ('verification_code', models.CharField(error_messages={'unique': 'This verification code already exists.'}, max_length=128, unique=True)), + ], + ), + migrations.RemoveField( + model_name='buildingurl', + name='building', + ), + migrations.RemoveConstraint( + model_name='building', + name='address_unique', + ), + migrations.AddField( + model_name='building', + name='bus', + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name='building', + name='public_id', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.AlterField( + model_name='building', + name='house_number', + field=models.PositiveIntegerField(), + ), + migrations.AddConstraint( + model_name='building', + constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('city'), django.db.models.functions.text.Lower('street'), django.db.models.functions.text.Lower('postal_code'), models.F('house_number'), django.db.models.functions.text.Lower('bus'), name='address_unique', violation_error_message='A building with this address already exists.'), + ), + migrations.DeleteModel( + name='BuildingURL', + ), + migrations.AddConstraint( + model_name='emailtemplate', + constraint=models.UniqueConstraint(models.F('name'), name='unique_template_name', violation_error_message='The name for this template already exists.'), + ), + ] diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py deleted file mode 100644 index e47b3aa5..00000000 --- a/backend/base/migrations/0002_emailtemplate_emailwhitelist_building_bus_and_more.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-18 00:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("base", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="EmailTemplate", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("name", models.CharField(max_length=40)), - ("template", models.TextField()), - ], - ), - migrations.CreateModel( - name="EmailWhitelist", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "email", - models.EmailField( - error_messages={"unique": "This email is already on the whitelist."}, - max_length=254, - unique=True, - verbose_name="email address", - ), - ), - ( - "verification_code", - models.CharField( - error_messages={"unique": "This verification code already exists."}, max_length=128, unique=True - ), - ), - ], - ), - migrations.AddField( - model_name="building", - name="bus", - field=models.CharField(blank=True, max_length=10, null=True), - ), - migrations.AddField( - model_name="building", - name="public_id", - field=models.CharField(blank=True, max_length=32, null=True), - ), - migrations.AlterField( - model_name="building", - name="house_number", - field=models.PositiveIntegerField(), - ), - migrations.DeleteModel( - name="BuildingURL", - ), - migrations.AddConstraint( - model_name="emailtemplate", - constraint=models.UniqueConstraint( - models.F("name"), - name="unique_template_name", - violation_error_message="The name for this template already exists.", - ), - ), - ] diff --git a/backend/base/models.py b/backend/base/models.py index 0c6504f8..33041994 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -136,7 +136,8 @@ class Meta: Lower("city"), Lower("street"), Lower("postal_code"), - Lower("house_number"), + "house_number", + Lower('bus'), name="address_unique", violation_error_message="A building with this address already exists.", ), From 394c31af1ec869702a66d9eaf79a0346896f3b97 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Mon, 20 Mar 2023 20:35:48 +0000 Subject: [PATCH 0114/1000] Auto formatted code --- ...2_emailtemplate_emailwhitelist_and_more.py | 74 ++++++++++++------- backend/base/models.py | 2 +- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py b/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py index cd18f20d..45f642dd 100644 --- a/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py +++ b/backend/base/migrations/0002_emailtemplate_emailwhitelist_and_more.py @@ -5,60 +5,84 @@ class Migration(migrations.Migration): - dependencies = [ - ('base', '0001_initial'), + ("base", "0001_initial"), ] operations = [ migrations.CreateModel( - name='EmailTemplate', + name="EmailTemplate", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40)), - ('template', models.TextField()), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=40)), + ("template", models.TextField()), ], ), migrations.CreateModel( - name='EmailWhitelist', + name="EmailWhitelist", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('email', models.EmailField(error_messages={'unique': 'This email is already on the whitelist.'}, max_length=254, unique=True, verbose_name='email address')), - ('verification_code', models.CharField(error_messages={'unique': 'This verification code already exists.'}, max_length=128, unique=True)), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "email", + models.EmailField( + error_messages={"unique": "This email is already on the whitelist."}, + max_length=254, + unique=True, + verbose_name="email address", + ), + ), + ( + "verification_code", + models.CharField( + error_messages={"unique": "This verification code already exists."}, max_length=128, unique=True + ), + ), ], ), migrations.RemoveField( - model_name='buildingurl', - name='building', + model_name="buildingurl", + name="building", ), migrations.RemoveConstraint( - model_name='building', - name='address_unique', + model_name="building", + name="address_unique", ), migrations.AddField( - model_name='building', - name='bus', + model_name="building", + name="bus", field=models.CharField(blank=True, max_length=10, null=True), ), migrations.AddField( - model_name='building', - name='public_id', + model_name="building", + name="public_id", field=models.CharField(blank=True, max_length=32, null=True), ), migrations.AlterField( - model_name='building', - name='house_number', + model_name="building", + name="house_number", field=models.PositiveIntegerField(), ), migrations.AddConstraint( - model_name='building', - constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('city'), django.db.models.functions.text.Lower('street'), django.db.models.functions.text.Lower('postal_code'), models.F('house_number'), django.db.models.functions.text.Lower('bus'), name='address_unique', violation_error_message='A building with this address already exists.'), + model_name="building", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("city"), + django.db.models.functions.text.Lower("street"), + django.db.models.functions.text.Lower("postal_code"), + models.F("house_number"), + django.db.models.functions.text.Lower("bus"), + name="address_unique", + violation_error_message="A building with this address already exists.", + ), ), migrations.DeleteModel( - name='BuildingURL', + name="BuildingURL", ), migrations.AddConstraint( - model_name='emailtemplate', - constraint=models.UniqueConstraint(models.F('name'), name='unique_template_name', violation_error_message='The name for this template already exists.'), + model_name="emailtemplate", + constraint=models.UniqueConstraint( + models.F("name"), + name="unique_template_name", + violation_error_message="The name for this template already exists.", + ), ), ] diff --git a/backend/base/models.py b/backend/base/models.py index 33041994..c2bb61e1 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -137,7 +137,7 @@ class Meta: Lower("street"), Lower("postal_code"), "house_number", - Lower('bus'), + Lower("bus"), name="address_unique", violation_error_message="A building with this address already exists.", ), From f0b599fef115351b00477f28952f644be9cd3bf4 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 21:50:56 +0100 Subject: [PATCH 0115/1000] ease of use by setting access token lifetime higher when in DEBUG mode --- backend/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/settings.py b/backend/config/settings.py index b6398c77..3c1a05bb 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -105,7 +105,7 @@ } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=100), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5 if not DEBUG else 100), 'REFRESH_TOKEN_LIFETIME': timedelta(days=14), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, From 6f5b67948932324e61a579b7ba00273b1c511b8f Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 22:10:14 +0100 Subject: [PATCH 0116/1000] API route for EmailTemplate (#90) --- backend/config/urls.py | 2 + backend/email_template/models.py | 3 - backend/email_template/urls.py | 13 ++++ backend/email_template/views.py | 88 ++++++++++++++++++++++++++- backend/util/request_response_util.py | 11 +++- 5 files changed, 110 insertions(+), 7 deletions(-) delete mode 100644 backend/email_template/models.py diff --git a/backend/config/urls.py b/backend/config/urls.py index 2d185cee..46545274 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -23,6 +23,7 @@ from building import urls as building_urls from building_comment import urls as building_comment_urls from building_on_tour import urls as building_on_tour_urls +from email_template import urls as email_template_urls from garbage_collection import urls as garbage_collection_urls from manual import urls as manual_urls from picture_building import urls as picture_building_urls @@ -42,6 +43,7 @@ path("picture_building/", include(picture_building_urls)), path("building/", include(building_urls)), path("building_comment/", include(building_comment_urls)), + path("email_template/", include(email_template_urls)), path("region/", include(region_urls)), path("garbage_collection/", include(garbage_collection_urls)), path("building_on_tour/", include(building_on_tour_urls)), diff --git a/backend/email_template/models.py b/backend/email_template/models.py deleted file mode 100644 index 71a83623..00000000 --- a/backend/email_template/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/backend/email_template/urls.py b/backend/email_template/urls.py index e69de29b..00be9ff3 100644 --- a/backend/email_template/urls.py +++ b/backend/email_template/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from .views import ( + DefaultEmailTemplate, + EmailTemplateIndividualView, + EmailTemplateAllView +) + +urlpatterns = [ + path("all/", EmailTemplateAllView.as_view()), + path("email_template//", EmailTemplateIndividualView.as_view()), + path("", DefaultEmailTemplate.as_view()) +] diff --git a/backend/email_template/views.py b/backend/email_template/views.py index 91ea44a2..182a1ee8 100644 --- a/backend/email_template/views.py +++ b/backend/email_template/views.py @@ -1,3 +1,87 @@ -from django.shortcuts import render +from drf_spectacular.utils import extend_schema +from rest_framework.views import APIView -# Create your views here. +from base.models import EmailTemplate +from base.serializers import EmailTemplateSerializer +from util.request_response_util import * + + +class DefaultEmailTemplate(APIView): + serializer_class = EmailTemplateSerializer + + @extend_schema(responses={201: EmailTemplateSerializer, 400: None}) + def post(self, request): + """ + Create a new EmailTemplate + """ + data = request_to_dict(request.data) + + email_template_instance = EmailTemplate() + + set_keys_of_instance(email_template_instance, data) + + if r := try_full_clean_and_save(email_template_instance): + return r + + return post_success(EmailTemplateSerializer(email_template_instance)) + + +class EmailTemplateIndividualView(APIView): + serializer_class = EmailTemplateSerializer + + @extend_schema(responses={200: EmailTemplateSerializer, 400: None}) + def get(self, request, email_template_id): + """ + Get info about an EmailTemplate with given id + """ + email_template_instance = EmailTemplate.objects.filter(id=email_template_id) + + if not email_template_instance: + return bad_request("EmailTemplate") + + return get_success(EmailTemplateSerializer(email_template_instance[0])) + + @extend_schema(responses={204: None, 400: None}) + def delete(self, request, email_template_id): + """ + Delete EmailTemplate with given id + """ + email_template_instance = EmailTemplate.objects.filter(id=email_template_id) + + if not email_template_instance: + return bad_request("EmailTemplate") + + email_template_instance[0].delete() + return delete_success() + + @extend_schema(responses={204: None, 400: None}) + def patch(self, request, email_template_id): + """ + Edit EmailTemplate with given id + """ + email_template_instance = EmailTemplate.objects.filter(id=email_template_id) + + if not email_template_instance: + return bad_request("EmailTemplate") + + email_template_instance = email_template_instance[0] + data = request_to_dict(request.data) + + set_keys_of_instance(email_template_instance, data) + + if r := try_full_clean_and_save(email_template_instance): + return r + + return patch_success(EmailTemplateSerializer(email_template_instance)) + + +class EmailTemplateAllView(APIView): + serializer_class = EmailTemplateSerializer + + def get(self, request): + """ + Get all EmailTemplates in the database + """ + email_template_instances = EmailTemplate.objects.all() + serializer = EmailTemplateSerializer(email_template_instances, many=True) + return get_success(serializer) diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index 919f7fd7..ee13541d 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -23,6 +23,13 @@ def bad_request(object_name="Object"): ) +def not_found(object_name="Object"): + return Response( + {"res", f"{object_name} with given ID does not exists."}, + status=status.HTTP_400_BAD_REQUEST + ) + + def bad_request_relation(object1: str, object2: str): return Response( {"res", f"There is no {object1} that is linked to {object2} with given id."}, @@ -41,8 +48,8 @@ def try_full_clean_and_save(model_instance, rm=False): # If body is empty, an attribute error is thrown in the clean function # if there is not checked whether the fields in self are intialized error_message = ( - str(e) - + ". This error could be thrown after you passed an empty body with e.g. a POST request." + str(e) + + ". This error could be thrown after you passed an empty body with e.g. a POST request." ) except (IntegrityError, ObjectDoesNotExist, ValueError) as e: error_message = str(e) From 12b079d28146a864e36a9a2f6f46cf63b5538857 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Mon, 20 Mar 2023 21:11:45 +0000 Subject: [PATCH 0117/1000] Auto formatted code --- backend/base/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index f934b348..36a5568f 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -13,7 +13,7 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2 ** 31 - 1 +MAX_INT = 2**31 - 1 def _check_for_present_keys(instance, keys_iterable): @@ -199,7 +199,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -373,9 +373,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 From a7951e43163666bfc2225d3e6e6ed31c0c2bd3a1 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 21:23:44 +0000 Subject: [PATCH 0118/1000] Auto formatted code --- backend/authentication/views.py | 11 +- backend/base/migrations/0001_initial.py | 48 ++--- backend/base/models.py | 33 +-- backend/building/views.py | 6 +- backend/building_comment/views.py | 16 +- backend/buildingurl/views.py | 5 +- backend/config/settings.py | 4 +- backend/config/urls.py | 4 +- backend/garbage_collection/views.py | 16 +- backend/picture_building/views.py | 16 +- backend/student_at_building_on_tour/views.py | 24 +-- backend/users/tests.py | 8 +- backend/util/request_response_util.py | 5 +- frontend/components/header/BaseHeader.tsx | 14 +- frontend/context/AuthProvider.tsx | 66 +++--- frontend/lib/login.tsx | 42 ++-- frontend/lib/reset.tsx | 36 ++-- frontend/lib/signup.tsx | 66 +++--- frontend/next.config.js | 18 +- frontend/pages/_app.tsx | 10 +- frontend/pages/_document.tsx | 18 +- frontend/pages/api/axios.tsx | 54 ++--- frontend/pages/index.tsx | 2 +- frontend/pages/login.tsx | 128 ++++++----- frontend/pages/reset-password.tsx | 102 +++++---- frontend/pages/signup.tsx | 210 +++++++++---------- frontend/pages/welcome.tsx | 118 +++++------ frontend/types.d.tsx | 16 +- 28 files changed, 475 insertions(+), 621 deletions(-) diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 16ab74db..542a8c8c 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -21,7 +21,6 @@ class LogoutViewWithBlacklisting(LogoutView): permission_classes = [IsAuthenticated] serializer_class = CookieTokenRefreshSerializer - @extend_schema(responses={200: None, 401: None, 500: None}) def logout(self, request): response = Response( @@ -38,16 +37,11 @@ def logout(self, request): token = RefreshToken(request.COOKIES.get(cookie_name)) token.blacklist() except KeyError: - response.data = { - "detail": _("Refresh token was not included in request cookies.") - } + response.data = {"detail": _("Refresh token was not included in request cookies.")} response.status_code = status.HTTP_401_UNAUTHORIZED except (TokenError, AttributeError, TypeError) as error: if hasattr(error, "args"): - if ( - "Token is blacklisted" in error.args - or "Token is invalid or expired" in error.args - ): + if "Token is blacklisted" in error.args or "Token is invalid or expired" in error.args: response.data = {"detail": _(error.args[0])} response.status_code = status.HTTP_401_UNAUTHORIZED else: @@ -78,7 +72,6 @@ def finalize_response(self, request, response, *args, **kwargs): class LoginViewWithHiddenTokens(LoginView): - def finalize_response(self, request, response, *args, **kwargs): if response.status_code == 200 and "access_token" in response.data: response.data["access_token"] = _("set successfully") diff --git a/backend/base/migrations/0001_initial.py b/backend/base/migrations/0001_initial.py index b7edefe0..1ac7d4c2 100644 --- a/backend/base/migrations/0001_initial.py +++ b/backend/base/migrations/0001_initial.py @@ -30,9 +30,7 @@ class Migration(migrations.Migration): ("password", models.CharField(max_length=128, verbose_name="password")), ( "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), + models.DateTimeField(blank=True, null=True, verbose_name="last login"), ), ( "is_superuser", @@ -45,9 +43,7 @@ class Migration(migrations.Migration): ( "email", models.EmailField( - error_messages={ - "unique": "A user already exists with this email." - }, + error_messages={"unique": "A user already exists with this email."}, max_length=254, unique=True, verbose_name="email address", @@ -59,9 +55,7 @@ class Migration(migrations.Migration): ("last_name", models.CharField(max_length=40)), ( "phone_number", - phonenumber_field.modelfields.PhoneNumberField( - max_length=128, region="BE" - ), + phonenumber_field.modelfields.PhoneNumberField(max_length=128, region="BE"), ), ], options={ @@ -179,9 +173,7 @@ class Migration(migrations.Migration): ("version_number", models.PositiveIntegerField(default=0)), ( "file", - models.FileField( - blank=True, null=True, upload_to="building_manuals/" - ), + models.FileField(blank=True, null=True, upload_to="building_manuals/"), ), ], ), @@ -199,9 +191,7 @@ class Migration(migrations.Migration): ), ( "picture", - models.ImageField( - blank=True, null=True, upload_to="building_pictures/" - ), + models.ImageField(blank=True, null=True, upload_to="building_pictures/"), ), ("description", models.TextField(blank=True, null=True)), ("timestamp", models.DateTimeField()), @@ -325,51 +315,37 @@ class Migration(migrations.Migration): migrations.AddField( model_name="picturebuilding", name="building", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.building" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.building"), ), migrations.AddField( model_name="manual", name="building", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.building" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.building"), ), migrations.AddField( model_name="garbagecollection", name="building", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.building" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.building"), ), migrations.AddField( model_name="buildingurl", name="building", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.building" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.building"), ), migrations.AddField( model_name="buildingontour", name="building", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.building" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.building"), ), migrations.AddField( model_name="buildingontour", name="tour", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.tour" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.tour"), ), migrations.AddField( model_name="buildingcomment", name="building", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="base.building" - ), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="base.building"), ), migrations.AddField( model_name="building", diff --git a/backend/base/models.py b/backend/base/models.py index ad9f004c..19f8294c 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -20,15 +20,11 @@ def _check_for_present_keys(instance, keys_iterable): for key in keys_iterable: if not vars(instance)[key]: - raise ValidationError( - f"Tried to access {key}, but it was not found in object" - ) + raise ValidationError(f"Tried to access {key}, but it was not found in object") class Region(models.Model): - region = models.CharField( - max_length=40, unique=True, error_messages={"unique": "Deze regio bestaat al."} - ) + region = models.CharField(max_length=40, unique=True, error_messages={"unique": "Deze regio bestaat al."}) def __str__(self): return self.region @@ -54,9 +50,7 @@ def clean(self): if Role.objects.count() != 0 and self.rank != MAX_INT: highest_rank = Role.objects.order_by("-rank").first().rank if self.rank > highest_rank + 1: - raise ValidationError( - f"The maximum rank allowed is {highest_rank + 1}." - ) + raise ValidationError(f"The maximum rank allowed is {highest_rank + 1}.") class Meta: constraints = [ @@ -250,15 +244,12 @@ def clean(self): building_region = self.building.region if tour_region != building_region: raise ValidationError( - f"The regions for tour ({tour_region}) en building ({building_region}) " - f"are different." + f"The regions for tour ({tour_region}) en building ({building_region}) " f"are different." ) nr_of_buildings = BuildingOnTour.objects.filter(tour=self.tour).count() if self.index > nr_of_buildings: - raise ValidationError( - f"The maximum allowed index for this building is {nr_of_buildings}" - ) + raise ValidationError(f"The maximum allowed index for this building is {nr_of_buildings}") def __str__(self): return f"{self.building} on tour {self.tour}, index: {self.index}" @@ -281,9 +272,7 @@ class Meta: class StudentAtBuildingOnTour(models.Model): - building_on_tour = models.ForeignKey( - BuildingOnTour, on_delete=models.SET_NULL, null=True - ) + building_on_tour = models.ForeignKey(BuildingOnTour, on_delete=models.SET_NULL, null=True) date = models.DateField() student = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) @@ -299,11 +288,7 @@ def clean(self): if user.role.name.lower() == "syndic": raise ValidationError("A syndic can't do tours") building_on_tour_region = self.building_on_tour.tour.region - if ( - not self.student.region.all() - .filter(region=building_on_tour_region) - .exists() - ): + if not self.student.region.all().filter(region=building_on_tour_region).exists(): raise ValidationError( f"Student ({user.email}) doesn't do tours in this region ({building_on_tour_region})." ) @@ -345,9 +330,7 @@ class PictureBuilding(models.Model): def clean(self): super().clean() - _check_for_present_keys( - self, {"building_id", "picture", "description", "timestamp"} - ) + _check_for_present_keys(self, {"building_id", "picture", "description", "timestamp"}) class Meta: constraints = [ diff --git a/backend/building/views.py b/backend/building/views.py index f7c435e6..e4f5296a 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -16,7 +16,6 @@ class DefaultBuilding(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = BuildingSerializer - @extend_schema(responses={201: BuildingSerializer, 400: None}) def post(self, request): """ @@ -36,12 +35,9 @@ def post(self, request): class BuildingIndividualView(APIView): - permission_classes = [IsAuthenticated, - IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding - ] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding] serializer_class = BuildingSerializer - @extend_schema(responses={200: BuildingSerializer, 400: None}) def get(self, request, building_id): """ diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index d91e0482..5f042c73 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -42,9 +42,7 @@ def get(self, request, building_comment_id): """ Get an invividual BuildingComment with given id """ - building_comment_instance = BuildingComment.objects.filter( - id=building_comment_id - ) + building_comment_instance = BuildingComment.objects.filter(id=building_comment_id) self.check_object_permissions(request, building_comment_instance.building) @@ -58,9 +56,7 @@ def delete(self, request, building_comment_id): """ Delete a BuildingComment with given id """ - building_comment_instance = BuildingComment.objectts.filter( - id=building_comment_id - ) + building_comment_instance = BuildingComment.objectts.filter(id=building_comment_id) self.check_object_permissions(request, building_comment_instance.building) @@ -75,9 +71,7 @@ def patch(self, request, building_comment_id): """ Edit BuildingComment with given id """ - building_comment_instance = BuildingComment.objects.filter( - id=building_comment_id - ) + building_comment_instance = BuildingComment.objects.filter(id=building_comment_id) if not building_comment_instance: return bad_request("BuildingComment") @@ -104,9 +98,7 @@ def get(self, request, building_id): """ Get all BuildingComments of building with given building id """ - building_comment_instance = BuildingComment.objects.filter( - building_id=building_id - ) + building_comment_instance = BuildingComment.objects.filter(building_id=building_id) if not building_comment_instance: return bad_request_relation("BuildingComment", "building") diff --git a/backend/buildingurl/views.py b/backend/buildingurl/views.py index d04e300b..51d3ed68 100644 --- a/backend/buildingurl/views.py +++ b/backend/buildingurl/views.py @@ -118,9 +118,7 @@ def get(self, request, syndic_id): self.check_object_permissions(request, id_holder) # All building IDs where user is syndic - building_ids = [ - building.id for building in Building.objects.filter(syndic=syndic_id) - ] + building_ids = [building.id for building in Building.objects.filter(syndic=syndic_id)] building_urls_instances = BuildingURL.objects.filter(building__in=building_ids) serializer = BuildingUrlSerializer(building_urls_instances, many=True) @@ -154,7 +152,6 @@ class BuildingUrlAllView(APIView): permission_classes = [IsAuthenticated, IsAdmin] serializer_class = BuildingUrlSerializer - def get(self, request): """ Get all building urls diff --git a/backend/config/settings.py b/backend/config/settings.py index 3394ce34..8ed4af01 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -77,9 +77,7 @@ "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], - "DEFAULT_AUTHENTICATION_CLASSES": ( - "dj_rest_auth.jwt_auth.JWTCookieAuthentication", - ), + "DEFAULT_AUTHENTICATION_CLASSES": ("dj_rest_auth.jwt_auth.JWTCookieAuthentication",), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } diff --git a/backend/config/urls.py b/backend/config/urls.py index 08c5ecda..c6cefd73 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -37,9 +37,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("docs/", SpectacularAPIView.as_view(), name="schema"), - path( - "docs/ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" - ), + path("docs/ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), path("authentication/", include(authentication_urls)), path("manual/", include(manual_urls)), path("picture_building/", include(picture_building_urls)), diff --git a/backend/garbage_collection/views.py b/backend/garbage_collection/views.py index 6b1ab4fe..98b35bb1 100644 --- a/backend/garbage_collection/views.py +++ b/backend/garbage_collection/views.py @@ -41,9 +41,7 @@ def get(self, request, garbage_collection_id): """ Get info about a garbage collection with given id """ - garbage_collection_instance = GarbageCollection.objects.filter( - id=garbage_collection_id - ) + garbage_collection_instance = GarbageCollection.objects.filter(id=garbage_collection_id) if not garbage_collection_instance: return bad_request("GarbageCollection") serializer = GarbageCollectionSerializer(garbage_collection_instance[0]) @@ -54,9 +52,7 @@ def delete(self, request, garbage_collection_id): """ Delete garbage collection with given id """ - garbage_collection_instance = GarbageCollection.objects.filter( - id=garbage_collection_id - ) + garbage_collection_instance = GarbageCollection.objects.filter(id=garbage_collection_id) if not garbage_collection_instance: return bad_request("GarbageCollection") garbage_collection_instance[0].delete() @@ -67,9 +63,7 @@ def patch(self, request, garbage_collection_id): """ Edit garbage collection with given id """ - garbage_collection_instance = GarbageCollection.objects.filter( - id=garbage_collection_id - ) + garbage_collection_instance = GarbageCollection.objects.filter(id=garbage_collection_id) if not garbage_collection_instance: return bad_request("GarbageCollection") @@ -116,7 +110,5 @@ def get(self, request): Get all garbage collections """ garbage_collection_instances = GarbageCollection.objects.all() - serializer = GarbageCollectionSerializer( - garbage_collection_instances, many=True - ) + serializer = GarbageCollectionSerializer(garbage_collection_instances, many=True) return get_success(serializer) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 5c121ac3..b70a08ff 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -39,9 +39,7 @@ def get(self, request, picture_building_id): """ Get PictureBuilding with given id """ - picture_building_instance = PictureBuilding.objects.filter( - id=picture_building_id - ) + picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) if len(picture_building_instance) != 1: return bad_request("PictureBuilding") @@ -57,9 +55,7 @@ def patch(self, request, picture_building_id): """ Edit info about PictureBuilding with given id """ - picture_building_instance = PictureBuilding.objects.filter( - id=picture_building_id - ) + picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) if not picture_building_instance: return bad_request("PictureBuilding") @@ -80,9 +76,7 @@ def delete(self, request, picture_building_id): """ delete a pictureBuilding from the database """ - picture_building_instance = PictureBuilding.objects.filter( - id=picture_building_id - ) + picture_building_instance = PictureBuilding.objects.filter(id=picture_building_id) if len(picture_building_instance) != 1: return bad_request("PictureBuilding") picture_building_instance = picture_building_instance[0] @@ -109,9 +103,7 @@ def get(self, request, building_id): self.check_object_permissions(request, building_instance) - picture_building_instances = PictureBuilding.objects.filter( - building_id=building_id - ) + picture_building_instances = PictureBuilding.objects.filter(building_id=building_id) serializer = PictureBuildingSerializer(picture_building_instances, many=True) return get_success(serializer) diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index 7b0bce33..1588f543 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -27,9 +27,7 @@ def post(self, request): if r := try_full_clean_and_save(student_at_building_on_tour_instance): return r - return post_success( - StudBuildTourSerializer(student_at_building_on_tour_instance) - ) + return post_success(StudBuildTourSerializer(student_at_building_on_tour_instance)) class BuildingTourPerStudentView(APIView): @@ -43,12 +41,8 @@ def get(self, request, student_id): id_holder = type("", (), {})() id_holder.id = student_id self.check_object_permissions(request, id_holder) - student_at_building_on_tour_instances = StudentAtBuildingOnTour.objects.filter( - student_id=student_id - ) - serializer = StudBuildTourSerializer( - student_at_building_on_tour_instances, many=True - ) + student_at_building_on_tour_instances = StudentAtBuildingOnTour.objects.filter(student_id=student_id) + serializer = StudBuildTourSerializer(student_at_building_on_tour_instances, many=True) return get_success(serializer) @@ -61,9 +55,7 @@ def get(self, request, student_at_building_on_tour_id): """ Get an individual StudentAtBuildingOnTour with given id """ - stud_tour_building_instance = StudentAtBuildingOnTour.objects.filter( - id=student_at_building_on_tour_id - ) + stud_tour_building_instance = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) if len(stud_tour_building_instance) != 1: return bad_request("StudentAtBuildingOnTour") @@ -79,9 +71,7 @@ def patch(self, request, student_at_building_on_tour_id): """ Edit info about an individual StudentAtBuildingOnTour with given id """ - stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter( - id=student_at_building_on_tour_id - ) + stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) if len(stud_tour_building_instances) != 1: return bad_request("StudentAtBuildingOnTour") @@ -105,9 +95,7 @@ def delete(self, request, student_at_building_on_tour_id): """ Delete StudentAtBuildingOnTour with given id """ - stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter( - id=student_at_building_on_tour_id - ) + stud_tour_building_instances = StudentAtBuildingOnTour.objects.filter(id=student_at_building_on_tour_id) if len(stud_tour_building_instances) != 1: return bad_request("StudentAtBuildingOnTour") stud_tour_building_instance = stud_tour_building_instances[0] diff --git a/backend/users/tests.py b/backend/users/tests.py index f9717bba..761517fc 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -25,9 +25,7 @@ def test_create_user(self): def test_create_superuser(self): User = get_user_model() - admin_user = User.objects.create_superuser( - email="super@user.com", password="foo" - ) + admin_user = User.objects.create_superuser(email="super@user.com", password="foo") self.assertEqual(admin_user.email, "super@user.com") self.assertTrue(admin_user.is_active) self.assertTrue(admin_user.is_staff) @@ -39,6 +37,4 @@ def test_create_superuser(self): except AttributeError: pass with self.assertRaises(ValueError): - User.objects.create_superuser( - email="super@user.com", password="foo", is_superuser=False - ) + User.objects.create_superuser(email="super@user.com", password="foo", is_superuser=False) diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index 919f7fd7..c0a2ff0c 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -40,10 +40,7 @@ def try_full_clean_and_save(model_instance, rm=False): except AttributeError as e: # If body is empty, an attribute error is thrown in the clean function # if there is not checked whether the fields in self are intialized - error_message = ( - str(e) - + ". This error could be thrown after you passed an empty body with e.g. a POST request." - ) + error_message = str(e) + ". This error could be thrown after you passed an empty body with e.g. a POST request." except (IntegrityError, ObjectDoesNotExist, ValueError) as e: error_message = str(e) finally: diff --git a/frontend/components/header/BaseHeader.tsx b/frontend/components/header/BaseHeader.tsx index 04b6d55f..ca3b2aef 100644 --- a/frontend/components/header/BaseHeader.tsx +++ b/frontend/components/header/BaseHeader.tsx @@ -4,13 +4,13 @@ import logo from "../../public/logo.png"; import styles from "./BaseHeader.module.css"; const BaseHeader = () => { - return ( -
-
- My App Logo -
-
- ); + return ( +
+
+ My App Logo +
+
+ ); }; export default BaseHeader; diff --git a/frontend/context/AuthProvider.tsx b/frontend/context/AuthProvider.tsx index 94b81e28..0eaf013f 100644 --- a/frontend/context/AuthProvider.tsx +++ b/frontend/context/AuthProvider.tsx @@ -1,43 +1,41 @@ import { createContext, ReactNode, useEffect, useState } from "react"; const AuthContext = createContext({ - auth: false, - loginUser: () => {}, - logoutUser: () => {}, + auth: false, + loginUser: () => {}, + logoutUser: () => {}, }); export default AuthContext; export const AuthProvider = ({ children }: { children: ReactNode }) => { - const [auth, setAuth] = useState(false); - - let loginUser = async () => { - setAuth(true); - }; - - let logoutUser = async () => { - setAuth(false); - sessionStorage.removeItem("auth"); - }; - - useEffect(() => { - const data = sessionStorage.getItem("auth"); - if (data) { - setAuth(JSON.parse(data)); - } - }, []); - - useEffect(() => { - sessionStorage.setItem("auth", JSON.stringify(auth)); - }, [auth]); - - let contextData = { - auth: auth, - loginUser: loginUser, - logoutUser: logoutUser, - }; - - return ( - {children} - ); + const [auth, setAuth] = useState(false); + + let loginUser = async () => { + setAuth(true); + }; + + let logoutUser = async () => { + setAuth(false); + sessionStorage.removeItem("auth"); + }; + + useEffect(() => { + const data = sessionStorage.getItem("auth"); + if (data) { + setAuth(JSON.parse(data)); + } + }, []); + + useEffect(() => { + sessionStorage.setItem("auth", JSON.stringify(auth)); + }, [auth]); + + let contextData = { + auth: auth, + loginUser: loginUser, + logoutUser: logoutUser, + }; + + return {children}; }; diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index 514650bc..25b4b69f 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -2,32 +2,26 @@ import api from "../pages/api/axios"; import { Login } from "@/types.d"; import { NextRouter } from "next/router"; -const login = async ( - email: string, - password: string, - router: NextRouter, - loginUser: () => void -): Promise => { - const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; - const login_data: Login = { - email: email, - password: password, - }; +const login = async (email: string, password: string, router: NextRouter, loginUser: () => void): Promise => { + const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; + const login_data: Login = { + email: email, + password: password, + }; - // Attempt to login with axios so authentication tokens get saved in our axios instance - api - .post(host, login_data, { - headers: { "Content-Type": "application/json" }, + // Attempt to login with axios so authentication tokens get saved in our axios instance + api.post(host, login_data, { + headers: { "Content-Type": "application/json" }, }) - .then((response: { status: number }) => { - if (response.status == 200) { - loginUser(); - router.push("/welcome"); - } - }) - .catch((error) => { - console.error(error); - }); + .then((response: { status: number }) => { + if (response.status == 200) { + loginUser(); + router.push("/welcome"); + } + }) + .catch((error) => { + console.error(error); + }); }; export default login; diff --git a/frontend/lib/reset.tsx b/frontend/lib/reset.tsx index 6653b98e..1e923048 100644 --- a/frontend/lib/reset.tsx +++ b/frontend/lib/reset.tsx @@ -2,28 +2,26 @@ import { Reset_Password } from "@/types.d"; import { NextRouter } from "next/router"; const reset = async (email: string, router: NextRouter): Promise => { - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}`; - const reset_data: Reset_Password = { - email: email, - }; + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}`; + const reset_data: Reset_Password = { + email: email, + }; - try { - // Request without axios because no authentication is needed for this POST request - const response = await fetch(host, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(reset_data), - }); + try { + // Request without axios because no authentication is needed for this POST request + const response = await fetch(host, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(reset_data), + }); - if (response.status == 200) { - alert( - "A password reset e-mail has been sent to the provided e-mail address" - ); - await router.push("/login"); + if (response.status == 200) { + alert("A password reset e-mail has been sent to the provided e-mail address"); + await router.push("/login"); + } + } catch (error) { + console.error(error); } - } catch (error) { - console.error(error); - } }; export default reset; diff --git a/frontend/lib/signup.tsx b/frontend/lib/signup.tsx index 5e0d7d94..5c259272 100644 --- a/frontend/lib/signup.tsx +++ b/frontend/lib/signup.tsx @@ -1,43 +1,43 @@ import { SignUp } from "@/types.d"; const signup = async ( - firstname: string, - lastname: string, - email: string, - password1: string, - password2: string, - router: any + firstname: string, + lastname: string, + email: string, + password1: string, + password2: string, + router: any ): Promise => { - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; - const signup_data: SignUp = { - first_name: firstname, - last_name: lastname, - email: email, - password1: password1, - password2: password2, - }; + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; + const signup_data: SignUp = { + first_name: firstname, + last_name: lastname, + email: email, + password1: password1, + password2: password2, + }; - // TODO Display error message from backend that will check this - // Small check if passwords are equal - if (signup_data.password1 !== signup_data.password2) { - alert("Passwords do not match"); - return; - } + // TODO Display error message from backend that will check this + // Small check if passwords are equal + if (signup_data.password1 !== signup_data.password2) { + alert("Passwords do not match"); + return; + } - try { - // Request without axios because no authentication is needed for this POST request - const response = await fetch(host, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(signup_data), - }); - if (response.status == 201) { - alert("Successfully created account"); - await router.push("/login"); + try { + // Request without axios because no authentication is needed for this POST request + const response = await fetch(host, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(signup_data), + }); + if (response.status == 201) { + alert("Successfully created account"); + await router.push("/login"); + } + } catch (error) { + console.error(error); } - } catch (error) { - console.error(error); - } }; export default signup; diff --git a/frontend/next.config.js b/frontend/next.config.js index b47f2f19..4a002860 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,15 +1,15 @@ /** @type {{redirects(): Promise<[{permanent: boolean, destination: string, source: string}]>}} */ const nextConfig = { - async redirects() { - return [ - { - source: "/", - destination: "/login", - permanent: true, - }, - ]; - }, + async redirects() { + return [ + { + source: "/", + destination: "/login", + permanent: true, + }, + ]; + }, }; module.exports = nextConfig; diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 5cfa7765..fd1da043 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -3,9 +3,9 @@ import type { AppProps } from "next/app"; import { AuthProvider } from "@/context/AuthProvider"; export default function App({ Component, pageProps }: AppProps) { - return ( - - - - ); + return ( + + + + ); } diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx index b2fff8b4..2f4bae41 100644 --- a/frontend/pages/_document.tsx +++ b/frontend/pages/_document.tsx @@ -1,13 +1,13 @@ import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { - return ( - - - -
- - - - ); + return ( + + + +
+ + + + ); } diff --git a/frontend/pages/api/axios.tsx b/frontend/pages/api/axios.tsx index 5a97ee09..543042a9 100644 --- a/frontend/pages/api/axios.tsx +++ b/frontend/pages/api/axios.tsx @@ -3,42 +3,42 @@ import { useRouter } from "next/router"; // Instance used to make authenticated requests const api = axios.create({ - baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, - withCredentials: true, + baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, + withCredentials: true, }); // Intercept on request and add access tokens to request api.interceptors.request.use( - (config) => { - return config; - }, - (error) => { - return Promise.reject(error); - } + (config) => { + return config; + }, + (error) => { + return Promise.reject(error); + } ); // Intercept on response and renew refresh token if necessary api.interceptors.response.use( - (response) => { - return response; - }, - async (error) => { - console.error(error.response); - if (error.response.status === 401 && !error.config._retry) { - error.config._retry = true; - try { - const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}`; - await api.post(request_url); - return api.request(error.config); - } catch (error) { - console.error(error); - const router = useRouter(); - await router.push("/login"); - throw error; - } + (response) => { + return response; + }, + async (error) => { + console.error(error.response); + if (error.response.status === 401 && !error.config._retry) { + error.config._retry = true; + try { + const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}`; + await api.post(request_url); + return api.request(error.config); + } catch (error) { + console.error(error); + const router = useRouter(); + await router.push("/login"); + throw error; + } + } + return Promise.reject(error); } - return Promise.reject(error); - } ); export default api; diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 89694c49..80022e33 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,5 +1,5 @@ function Home() { - return null; // instant redirect to login page + return null; // instant redirect to login page } export default Home; diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 04a1d9c0..6e614c4c 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -9,75 +9,67 @@ import { useRouter } from "next/router"; import AuthContext from "@/context/AuthProvider"; export default function Login() { - let { loginUser } = useContext(AuthContext); + let { loginUser } = useContext(AuthContext); - const router = useRouter(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); + const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); - const handleSubmit = async (event: FormEvent): Promise => { - event.preventDefault(); - try { - await login(username, password, router, loginUser); - } catch (error) { - console.error(error); - } - }; + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + try { + await login(username, password, router, loginUser); + } catch (error) { + console.error(error); + } + }; - return ( - <> - -
-
- My App Logo -
-
-

Login.

-
- - ) => - setUsername(e.target.value) - } - /> - - ) => - setPassword(e.target.value) - } - required - /> - -
-

- - Forgot Password - -

-

- Don't have an account?{" "} - - Sign up here - -

-
-
- - ); + return ( + <> + +
+
+ My App Logo +
+
+

Login.

+
+ + ) => setUsername(e.target.value)} + /> + + ) => setPassword(e.target.value)} + required + /> + +
+

+ + Forgot Password + +

+

+ Don't have an account?{" "} + + Sign up here + +

+
+
+ + ); } diff --git a/frontend/pages/reset-password.tsx b/frontend/pages/reset-password.tsx index 8554d413..4c3b89b2 100644 --- a/frontend/pages/reset-password.tsx +++ b/frontend/pages/reset-password.tsx @@ -8,62 +8,54 @@ import Link from "next/link"; import reset from "@/lib/reset"; export default function ResetPassword() { - const router = useRouter(); - const [email, setEmail] = useState(""); + const router = useRouter(); + const [email, setEmail] = useState(""); - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - try { - await reset(email, router); - } catch (error) { - console.error(error); - } - }; + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + await reset(email, router); + } catch (error) { + console.error(error); + } + }; - return ( - <> - -
-
- My App Logo -
-
-

- Enter your e-mail in order to find your account -

-
-
- - ) => - setEmail(e.target.value) - } - required - /> + return ( + <> + +
+
+ My App Logo +
+
+

Enter your e-mail in order to find your account

+
+ + + ) => setEmail(e.target.value)} + required + /> - - -

- Already have an account?{" "} - - Log in here - -

-
-
- - ); + + +

+ Already have an account?{" "} + + Log in here + +

+
+
+ + ); } diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 4e3599d8..82eea0cf 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -8,123 +8,109 @@ import Link from "next/link"; import signup from "@/lib/signup"; export default function Signup() { - const router = useRouter(); - const [firstname, setFirstname] = useState(""); - const [lastname, setLastname] = useState(""); - const [email, setEmail] = useState(""); - const [password1, setPassword1] = useState(""); - const [password2, setPassword2] = useState(""); + const router = useRouter(); + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); + const [email, setEmail] = useState(""); + const [password1, setPassword1] = useState(""); + const [password2, setPassword2] = useState(""); - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - try { - await signup(firstname, lastname, email, password1, password2, router); - } catch (error) { - console.error(error); - } - }; + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + await signup(firstname, lastname, email, password1, password2, router); + } catch (error) { + console.error(error); + } + }; - return ( - <> - -
-
- My App Logo -
-
-

Signup

-
- - ) => - setFirstname(e.target.value) - } - required - /> + return ( + <> + +
+
+ My App Logo +
+
+

Signup

+ + + ) => setFirstname(e.target.value)} + required + /> - - ) => - setLastname(e.target.value) - } - required - /> + + ) => setLastname(e.target.value)} + required + /> - - ) => - setEmail(e.target.value) - } - required - /> + + ) => setEmail(e.target.value)} + required + /> - - ) => - setPassword1(e.target.value) - } - required - /> + + ) => setPassword1(e.target.value)} + required + /> - - ) => - setPassword2(e.target.value) - } - required - /> + + ) => setPassword2(e.target.value)} + required + /> - - -

- Already have an account?{" "} - - Log in here - -

-
-
- - ); + + +

+ Already have an account?{" "} + + Log in here + +

+
+
+ + ); } diff --git a/frontend/pages/welcome.tsx b/frontend/pages/welcome.tsx index 9af7cd92..fc7f4847 100644 --- a/frontend/pages/welcome.tsx +++ b/frontend/pages/welcome.tsx @@ -8,74 +8,70 @@ import { useRouter } from "next/router"; import AuthContext from "@/context/AuthProvider"; function Welcome() { - let { auth, logoutUser } = useContext(AuthContext); - const router = useRouter(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); // prevents preview welcome page before auth check + let { auth, logoutUser } = useContext(AuthContext); + const router = useRouter(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); // prevents preview welcome page before auth check - useEffect(() => { - let present = "auth" in sessionStorage; - let value = sessionStorage.getItem("auth"); - if (present && value == "true") { - setLoading(false); - fetchData(); - } else { - router.push("/login"); - } - }, []); - - async function fetchData() { - try { - api - .get( - `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}` - ) - .then((info) => { - if (!info.data || info.data.length === 0) { + useEffect(() => { + let present = "auth" in sessionStorage; + let value = sessionStorage.getItem("auth"); + if (present && value == "true") { + setLoading(false); + fetchData(); + } else { router.push("/login"); - } else { - setData(info.data); - } - }); - } catch (error) { - console.error(error); - } - } + } + }, []); - const handleLogout = async () => { - try { - const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); - if (response.status === 200) { - logoutUser(); - await router.push("/login"); - } - } catch (error) { - console.error(error); + async function fetchData() { + try { + api.get(`${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}`).then((info) => { + if (!info.data || info.data.length === 0) { + router.push("/login"); + } else { + setData(info.data); + } + }); + } catch (error) { + console.error(error); + } } - }; - return ( - <> - {loading ? ( -
Loading...
- ) : ( + const handleLogout = async () => { + try { + const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); + if (response.status === 200) { + logoutUser(); + await router.push("/login"); + } + } catch (error) { + console.error(error); + } + }; + + return ( <> - -

Welcome!

- Site coming soon - -

Users:

-
    - {data.map((item, index) => ( -
  • {JSON.stringify(item)}
  • - ))} -
+ {loading ? ( +
Loading...
+ ) : ( + <> + +

Welcome!

+ Site coming soon + +

Users:

+
    + {data.map((item, index) => ( +
  • {JSON.stringify(item)}
  • + ))} +
+ + )} - )} - - ); + ); } export default Welcome; diff --git a/frontend/types.d.tsx b/frontend/types.d.tsx index 32968a26..f71854af 100644 --- a/frontend/types.d.tsx +++ b/frontend/types.d.tsx @@ -1,16 +1,16 @@ export type Login = { - email: string; - password: string; + email: string; + password: string; }; export type SignUp = { - first_name: string; - last_name: string; - email: string; - password1: string; - password2: string; + first_name: string; + last_name: string; + email: string; + password1: string; + password2: string; }; export type Reset_Password = { - email: string; + email: string; }; From 5e13a9196c6513cab2078eb2a7bd375bd52cc52a Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 22:42:53 +0100 Subject: [PATCH 0119/1000] more strict edit/delete policy --- backend/authorisation/permissions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 32d0e620..989f9bd8 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -125,10 +125,10 @@ class CanEditUser(BasePermission): """ Checks if the user has the right permissions to edit """ - message = "You don't have the right permissions to edit the user accordingly" + message = "You don't have the right permissions to edit/delete this user" def has_object_permission(self, request, view, obj: User): - if request.method == 'PATCH': + if request.method in ['PATCH', 'DELETE']: return request.user.id == obj.id or request.user.role.rank < obj.role.rank return True From ba6ebf97a7c5379659f11f6b0360d2478ed53708 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 22:59:58 +0100 Subject: [PATCH 0120/1000] user can't delete himself (only someone of higher rank can) --- backend/authorisation/permissions.py | 16 ++++++++++++++-- backend/users/views.py | 7 ++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 989f9bd8..6f267aa0 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -125,14 +125,26 @@ class CanEditUser(BasePermission): """ Checks if the user has the right permissions to edit """ - message = "You don't have the right permissions to edit/delete this user" + message = "You don't have the right permissions to edit this user" def has_object_permission(self, request, view, obj: User): - if request.method in ['PATCH', 'DELETE']: + if request.method in ['PATCH']: return request.user.id == obj.id or request.user.role.rank < obj.role.rank return True +class CanDeleteUser(BasePermission): + """ + Checks if the user has the right permissions to delete a user + """ + message = "You don't have the right permissions to delete this user" + + def has_object_permission(self, request, view, obj: User): + if request.method in ['DELETE']: + return request.user.role.rank < obj.role.rank + return True + + class CanEditRole(BasePermission): """ Checks if the user has the right permissions to edit the role of a user diff --git a/backend/users/views.py b/backend/users/views.py index f8d164b2..4aed6d7f 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,13 +3,12 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, CanEditUser, CanEditRole +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, CanEditUser, CanEditRole, CanDeleteUser from base.models import User from base.serializers import UserSerializer from util.request_response_util import * from drf_spectacular.utils import extend_schema - TRANSLATE = {"role": "role_id"} @@ -64,7 +63,9 @@ def post(self, request): class UserIndividualView(APIView): - permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerAccount, CanEditUser, CanEditRole] + permission_classes = [IsAuthenticated, + IsAdmin | IsSuperStudent | OwnerAccount, + CanEditUser, CanEditRole, CanDeleteUser] serializer_class = UserSerializer @extend_schema(responses={200: UserSerializer, 400: None}) From 72b63fbe17b86472eeef1f480bf620bf8f1aa87d Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 23:07:09 +0100 Subject: [PATCH 0121/1000] Added check to EmailWhitelist: you cannot whitelist an email that is already linked to a user (#90) --- backend/base/models.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index 36a5568f..0a3f0553 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -13,7 +13,7 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2**31 - 1 +MAX_INT = 2 ** 31 - 1 def _check_for_present_keys(instance, keys_iterable): @@ -98,6 +98,11 @@ class EmailWhitelist(models.Model): max_length=128, unique=True, error_messages={"unique": "This verification code already exists."} ) + def clean(self): + _check_for_present_keys(self, {"email"}) + if User.objects.filter(email=self.email): + raise ValidationError("Email already exists in database (the email is already linked to a user)") + class Building(models.Model): city = models.CharField(max_length=40) @@ -199,7 +204,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -373,9 +378,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 From 901626ccc8e7b50a38ae7652bb6f1ffb959a615e Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Mon, 20 Mar 2023 23:12:28 +0100 Subject: [PATCH 0122/1000] Provided a function to generate a uuid (#90) --- backend/util/request_response_util.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index ee13541d..400156e5 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -2,6 +2,19 @@ from django.db import IntegrityError from rest_framework import status from rest_framework.response import Response +import uuid +from typing import Callable + + +def get_unique_uuid(lookup_func: Callable[[str], bool] = None): + # https://docs.python.org/3/library/uuid.html + out_id = uuid.uuid4().hex + + # Normally it should never happen that the generated `id` is not unique, + # but just to be theoretically sure, you can pass a function that checks if the uuid is already in the database + while lookup_func and lookup_func(out_id): + out_id = uuid.uuid4().hex + return out_id def set_keys_of_instance(instance, data: dict, translation: dict = {}): From 86af65932b3ffae1c2352e5133f07593aa8e3290 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 23:13:13 +0100 Subject: [PATCH 0123/1000] user can't create user with higher rank than himself --- backend/authorisation/permissions.py | 15 +++++++++++++++ backend/users/views.py | 7 +++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 6f267aa0..c7154760 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -121,6 +121,21 @@ def has_object_permission(self, request, view, obj: User): return False +class CanCreateUser(BasePermission): + """ + Checks if the user has the right permissions to create the user + """ + message = "You can't create a user of a higher role" + + def has_object_permission(self, request, view, obj: User): + if request.method in ['POST']: + data = request_to_dict(request.data) + if 'role' in data.keys(): + role_instance = Role.objects.filter(id=data['role'])[0] + return request.user.role.rank <= role_instance.rank + return True + + class CanEditUser(BasePermission): """ Checks if the user has the right permissions to edit diff --git a/backend/users/views.py b/backend/users/views.py index 4aed6d7f..d42760dd 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,7 +3,8 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, CanEditUser, CanEditRole, CanDeleteUser +from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, CanEditUser, CanEditRole, CanDeleteUser, \ + CanCreateUser from base.models import User from base.serializers import UserSerializer from util.request_response_util import * @@ -31,7 +32,7 @@ def _try_adding_region_to_user_instance(user_instance, region_value): class DefaultUser(APIView): - permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent, CanCreateUser] serializer_class = UserSerializer # TODO: in order for this to work, you have to pass a password @@ -47,6 +48,8 @@ def post(self, request): set_keys_of_instance(user_instance, data, TRANSLATE) + self.check_object_permissions(request, user_instance) + if r := try_full_clean_and_save(user_instance): return r From 443950fb068939a1ec3e72c7f9396a4248e4d10d Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Mon, 20 Mar 2023 23:53:35 +0100 Subject: [PATCH 0124/1000] user can't change his/her own role --- backend/authorisation/permissions.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index c7154760..70ad2435 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -136,27 +136,27 @@ def has_object_permission(self, request, view, obj: User): return True -class CanEditUser(BasePermission): +class CanDeleteUser(BasePermission): """ - Checks if the user has the right permissions to edit + Checks if the user has the right permissions to delete a user """ - message = "You don't have the right permissions to edit this user" + message = "You don't have the right permissions to delete this user" def has_object_permission(self, request, view, obj: User): - if request.method in ['PATCH']: - return request.user.id == obj.id or request.user.role.rank < obj.role.rank + if request.method in ['DELETE']: + return request.user.role.rank < obj.role.rank return True -class CanDeleteUser(BasePermission): +class CanEditUser(BasePermission): """ - Checks if the user has the right permissions to delete a user + Checks if the user has the right permissions to edit """ - message = "You don't have the right permissions to delete this user" + message = "You don't have the right permissions to edit this user" def has_object_permission(self, request, view, obj: User): - if request.method in ['DELETE']: - return request.user.role.rank < obj.role.rank + if request.method in ['PATCH']: + return request.user.id == obj.id or request.user.role.rank < obj.role.rank return True @@ -164,12 +164,15 @@ class CanEditRole(BasePermission): """ Checks if the user has the right permissions to edit the role of a user """ - message = "You can't assign a role that is higher that your own" + message = "You can't assign a role to yourself or assign a role tha is higher than your own" def has_object_permission(self, request, view, obj: User): if request.method in ['PATCH']: data = request_to_dict(request.data) if 'role' in data.keys(): + if request.user.id == obj.id: + # you aren't allowed to change your own role + return False role_instance = Role.objects.filter(id=data['role'])[0] return request.user.role.rank <= role_instance.rank return True From f2da46c03894adcc553caffee097bd438b5857db Mon Sep 17 00:00:00 2001 From: TiboStr Date: Mon, 20 Mar 2023 23:13:14 +0000 Subject: [PATCH 0125/1000] Auto formatted code --- backend/authorisation/permissions.py | 50 ++++++++++++++++++---------- backend/users/views.py | 21 +++++++++--- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/backend/authorisation/permissions.py b/backend/authorisation/permissions.py index 70ad2435..8f84aa80 100644 --- a/backend/authorisation/permissions.py +++ b/backend/authorisation/permissions.py @@ -2,7 +2,7 @@ from base.models import Building, User, Role from util.request_response_util import request_to_dict -SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] +SAFE_METHODS = ["GET", "HEAD", "OPTIONS"] # ---------------------- @@ -12,51 +12,56 @@ class IsAdmin(BasePermission): """ Global permission that only grants access to admin users """ + message = "Admin permission required" def has_permission(self, request, view): - return request.user.role.name.lower() == 'admin' + return request.user.role.name.lower() == "admin" class IsSuperStudent(BasePermission): """ Global permission that grants access to super students """ + message = "Super student permission required" def has_permission(self, request, view): - return request.user.role.name.lower() == 'superstudent' + return request.user.role.name.lower() == "superstudent" class IsStudent(BasePermission): """ Global permission that grants access to students """ + message = "Student permission required" def has_permission(self, request, view): - return request.user.role.name.lower() == 'student' + return request.user.role.name.lower() == "student" class ReadOnlyStudent(BasePermission): """ Global permission that only grants read access for students """ + message = "Students are only allowed to read" def has_permission(self, request, view): if request.method in SAFE_METHODS: - return request.user.role.name.lower() == 'student' + return request.user.role.name.lower() == "student" class IsSyndic(BasePermission): """ Global permission that grants access to syndicates """ + message = "Syndic permission required" def has_permission(self, request, view): - return request.user.role.name.lower() == 'syndic' + return request.user.role.name.lower() == "syndic" # ------------------ @@ -71,14 +76,16 @@ def has_permission(self, request, view): # OBJECT PERMISSIONS # ------------------ + class OwnerOfBuilding(BasePermission): """ Check if the user owns the building """ + message = "You can only access/edit the buildings that you own" def has_permission(self, request, view): - return request.user.role.name.lower() == 'syndic' + return request.user.role.name.lower() == "syndic" def has_object_permission(self, request, view, obj: Building): return request.user.id == obj.syndic_id @@ -88,10 +95,11 @@ class ReadOnlyOwnerOfBuilding(BasePermission): """ Checks if the user owns the building and only tries to read from it """ + message = "You can only read the building that you own" def has_permission(self, request, view): - return request.user.role.name.lower() == 'syndic' + return request.user.role.name.lower() == "syndic" def has_object_permission(self, request, view, obj: Building): if request.method in SAFE_METHODS: @@ -103,6 +111,7 @@ class OwnerAccount(BasePermission): """ Checks if the user is owns the user account """ + message = "You can only access/edit your own account" def has_object_permission(self, request, view, obj: User): @@ -111,8 +120,9 @@ def has_object_permission(self, request, view, obj: User): class ReadOnlyOwnerAccount(BasePermission): """ - Checks if the user is owns the user account - """ + Checks if the user is owns the user account + """ + message = "You can only access/edit your own account" def has_object_permission(self, request, view, obj: User): @@ -125,13 +135,14 @@ class CanCreateUser(BasePermission): """ Checks if the user has the right permissions to create the user """ + message = "You can't create a user of a higher role" def has_object_permission(self, request, view, obj: User): - if request.method in ['POST']: + if request.method in ["POST"]: data = request_to_dict(request.data) - if 'role' in data.keys(): - role_instance = Role.objects.filter(id=data['role'])[0] + if "role" in data.keys(): + role_instance = Role.objects.filter(id=data["role"])[0] return request.user.role.rank <= role_instance.rank return True @@ -140,10 +151,11 @@ class CanDeleteUser(BasePermission): """ Checks if the user has the right permissions to delete a user """ + message = "You don't have the right permissions to delete this user" def has_object_permission(self, request, view, obj: User): - if request.method in ['DELETE']: + if request.method in ["DELETE"]: return request.user.role.rank < obj.role.rank return True @@ -152,10 +164,11 @@ class CanEditUser(BasePermission): """ Checks if the user has the right permissions to edit """ + message = "You don't have the right permissions to edit this user" def has_object_permission(self, request, view, obj: User): - if request.method in ['PATCH']: + if request.method in ["PATCH"]: return request.user.id == obj.id or request.user.role.rank < obj.role.rank return True @@ -164,15 +177,16 @@ class CanEditRole(BasePermission): """ Checks if the user has the right permissions to edit the role of a user """ + message = "You can't assign a role to yourself or assign a role tha is higher than your own" def has_object_permission(self, request, view, obj: User): - if request.method in ['PATCH']: + if request.method in ["PATCH"]: data = request_to_dict(request.data) - if 'role' in data.keys(): + if "role" in data.keys(): if request.user.id == obj.id: # you aren't allowed to change your own role return False - role_instance = Role.objects.filter(id=data['role'])[0] + role_instance = Role.objects.filter(id=data["role"])[0] return request.user.role.rank <= role_instance.rank return True diff --git a/backend/users/views.py b/backend/users/views.py index d42760dd..aea4bc5e 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,8 +3,15 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from authorisation.permissions import IsAdmin, IsSuperStudent, OwnerAccount, CanEditUser, CanEditRole, CanDeleteUser, \ - CanCreateUser +from authorisation.permissions import ( + IsAdmin, + IsSuperStudent, + OwnerAccount, + CanEditUser, + CanEditRole, + CanDeleteUser, + CanCreateUser, +) from base.models import User from base.serializers import UserSerializer from util.request_response_util import * @@ -66,9 +73,13 @@ def post(self, request): class UserIndividualView(APIView): - permission_classes = [IsAuthenticated, - IsAdmin | IsSuperStudent | OwnerAccount, - CanEditUser, CanEditRole, CanDeleteUser] + permission_classes = [ + IsAuthenticated, + IsAdmin | IsSuperStudent | OwnerAccount, + CanEditUser, + CanEditRole, + CanDeleteUser, + ] serializer_class = UserSerializer @extend_schema(responses={200: UserSerializer, 400: None}) From 3083bff00035a225ae29171bfa77d617972f01cb Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Tue, 21 Mar 2023 00:34:17 +0100 Subject: [PATCH 0126/1000] Fixed axios interceptor --- frontend/components/header/BaseHeader.tsx | 29 ++-- frontend/context/AuthProvider.tsx | 43 ----- frontend/lib/login.tsx | 50 +++--- frontend/lib/reset.tsx | 45 +++-- frontend/lib/signup.tsx | 68 ++++---- frontend/next.config.js | 12 +- frontend/pages/_app.tsx | 15 +- frontend/pages/_document.tsx | 20 +-- frontend/pages/api/axios.tsx | 54 +++--- frontend/pages/index.tsx | 3 +- frontend/pages/login.tsx | 126 ++++++-------- frontend/pages/reset-password.tsx | 97 +++++------ frontend/pages/signup.tsx | 195 +++++++++------------- frontend/pages/welcome.tsx | 114 ++++++------- frontend/types.d.tsx | 22 +-- 15 files changed, 369 insertions(+), 524 deletions(-) delete mode 100644 frontend/context/AuthProvider.tsx diff --git a/frontend/components/header/BaseHeader.tsx b/frontend/components/header/BaseHeader.tsx index 04b6d55f..be3f05ce 100644 --- a/frontend/components/header/BaseHeader.tsx +++ b/frontend/components/header/BaseHeader.tsx @@ -1,16 +1,21 @@ -import React from "react"; -import Image from "next/image"; -import logo from "../../public/logo.png"; -import styles from "./BaseHeader.module.css"; +import React from 'react'; +import Image from 'next/image' +import logo from '../../public/logo.png' +import styles from './BaseHeader.module.css'; const BaseHeader = () => { - return ( -
-
- My App Logo -
-
- ); + return ( +
+
+ My App Logo +
+
+ ); }; -export default BaseHeader; +export default BaseHeader; \ No newline at end of file diff --git a/frontend/context/AuthProvider.tsx b/frontend/context/AuthProvider.tsx deleted file mode 100644 index 94b81e28..00000000 --- a/frontend/context/AuthProvider.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { createContext, ReactNode, useEffect, useState } from "react"; - -const AuthContext = createContext({ - auth: false, - loginUser: () => {}, - logoutUser: () => {}, -}); - -export default AuthContext; - -export const AuthProvider = ({ children }: { children: ReactNode }) => { - const [auth, setAuth] = useState(false); - - let loginUser = async () => { - setAuth(true); - }; - - let logoutUser = async () => { - setAuth(false); - sessionStorage.removeItem("auth"); - }; - - useEffect(() => { - const data = sessionStorage.getItem("auth"); - if (data) { - setAuth(JSON.parse(data)); - } - }, []); - - useEffect(() => { - sessionStorage.setItem("auth", JSON.stringify(auth)); - }, [auth]); - - let contextData = { - auth: auth, - loginUser: loginUser, - logoutUser: logoutUser, - }; - - return ( - {children} - ); -}; diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index 514650bc..86f7eaf5 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -1,33 +1,27 @@ -import api from "../pages/api/axios"; -import { Login } from "@/types.d"; -import { NextRouter } from "next/router"; +import api from '../pages/api/axios'; +import {Login} from "@/types.d"; +import {NextRouter} from "next/router"; -const login = async ( - email: string, - password: string, - router: NextRouter, - loginUser: () => void -): Promise => { - const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; - const login_data: Login = { - email: email, - password: password, - }; +const login = async (email: string, password: string, router: NextRouter): Promise => { - // Attempt to login with axios so authentication tokens get saved in our axios instance - api - .post(host, login_data, { - headers: { "Content-Type": "application/json" }, - }) - .then((response: { status: number }) => { - if (response.status == 200) { - loginUser(); - router.push("/welcome"); - } + const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; + const login_data: Login = { + email: email, + password: password, + }; + + // Attempt to login with axios so authentication tokens get saved in our axios instance + api.post(host, login_data, { + headers: {'Content-Type': 'application/json'} }) - .catch((error) => { - console.error(error); - }); + .then((response: { status: number }) => { + if (response.status == 200) { + router.push('/welcome'); + } + }) + .catch((error) => { + console.error(error); + }); }; -export default login; +export default login; \ No newline at end of file diff --git a/frontend/lib/reset.tsx b/frontend/lib/reset.tsx index 6653b98e..f4feb160 100644 --- a/frontend/lib/reset.tsx +++ b/frontend/lib/reset.tsx @@ -1,29 +1,28 @@ -import { Reset_Password } from "@/types.d"; -import { NextRouter } from "next/router"; +import {Reset_Password} from "@/types.d"; +import {NextRouter} from "next/router"; const reset = async (email: string, router: NextRouter): Promise => { - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}`; - const reset_data: Reset_Password = { - email: email, - }; - try { - // Request without axios because no authentication is needed for this POST request - const response = await fetch(host, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(reset_data), - }); + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}` + const reset_data: Reset_Password = { + email: email, + } + + try { + // Request without axios because no authentication is needed for this POST request + const response = await fetch(host, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(reset_data), + }); - if (response.status == 200) { - alert( - "A password reset e-mail has been sent to the provided e-mail address" - ); - await router.push("/login"); + if (response.status == 200) { + alert("A password reset e-mail has been sent to the provided e-mail address"); + await router.push('/login'); + } + } catch (error) { + console.error(error); } - } catch (error) { - console.error(error); - } -}; +} -export default reset; +export default reset; \ No newline at end of file diff --git a/frontend/lib/signup.tsx b/frontend/lib/signup.tsx index 5e0d7d94..b036cc1b 100644 --- a/frontend/lib/signup.tsx +++ b/frontend/lib/signup.tsx @@ -1,43 +1,37 @@ -import { SignUp } from "@/types.d"; +import {SignUp} from "@/types.d"; -const signup = async ( - firstname: string, - lastname: string, - email: string, - password1: string, - password2: string, - router: any -): Promise => { - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; - const signup_data: SignUp = { - first_name: firstname, - last_name: lastname, - email: email, - password1: password1, - password2: password2, - }; +const signup = async (firstname: string, lastname: string, email: string, password1: string, password2: string, router: any): Promise => { - // TODO Display error message from backend that will check this - // Small check if passwords are equal - if (signup_data.password1 !== signup_data.password2) { - alert("Passwords do not match"); - return; - } + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; + const signup_data: SignUp = { + first_name: firstname, + last_name: lastname, + email: email, + password1: password1, + password2: password2, + } + + // TODO Display error message from backend that will check this + // Small check if passwords are equal + if (signup_data.password1 !== signup_data.password2) { + alert("Passwords do not match"); + return; + } - try { - // Request without axios because no authentication is needed for this POST request - const response = await fetch(host, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(signup_data), - }); - if (response.status == 201) { - alert("Successfully created account"); - await router.push("/login"); + try { + // Request without axios because no authentication is needed for this POST request + const response = await fetch(host, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(signup_data), + }); + if (response.status == 201) { + alert("Successfully created account"); + await router.push("/login"); + } + } catch (error) { + console.error(error); } - } catch (error) { - console.error(error); - } -}; +} export default signup; diff --git a/frontend/next.config.js b/frontend/next.config.js index b47f2f19..03ab1813 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -4,12 +4,12 @@ const nextConfig = { async redirects() { return [ { - source: "/", - destination: "/login", + source: '/', + destination: '/login', permanent: true, }, - ]; - }, -}; + ] + } +} -module.exports = nextConfig; +module.exports = nextConfig diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 5cfa7765..94252577 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,11 +1,8 @@ -import "@/styles/globals.css"; -import type { AppProps } from "next/app"; -import { AuthProvider } from "@/context/AuthProvider"; +import '@/styles/globals.css' +import type {AppProps} from 'next/app' -export default function App({ Component, pageProps }: AppProps) { - return ( - - - - ); +export default function App({Component, pageProps}: AppProps) { + return ( + + ); } diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx index b2fff8b4..81f4832d 100644 --- a/frontend/pages/_document.tsx +++ b/frontend/pages/_document.tsx @@ -1,13 +1,13 @@ -import { Html, Head, Main, NextScript } from "next/document"; +import {Html, Head, Main, NextScript} from 'next/document' export default function Document() { - return ( - - - -
- - - - ); + return ( + + + +
+ + + + ) } diff --git a/frontend/pages/api/axios.tsx b/frontend/pages/api/axios.tsx index 5a97ee09..3c5d62ba 100644 --- a/frontend/pages/api/axios.tsx +++ b/frontend/pages/api/axios.tsx @@ -1,44 +1,30 @@ -import axios from "axios"; -import { useRouter } from "next/router"; +import axios from 'axios'; +axios.defaults.withCredentials = true; // Instance used to make authenticated requests const api = axios.create({ - baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, - withCredentials: true, + baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, + withCredentials: true }); -// Intercept on request and add access tokens to request -api.interceptors.request.use( - (config) => { - return config; - }, - (error) => { - return Promise.reject(error); - } -); - // Intercept on response and renew refresh token if necessary api.interceptors.response.use( - (response) => { - return response; - }, - async (error) => { - console.error(error.response); - if (error.response.status === 401 && !error.config._retry) { - error.config._retry = true; - try { - const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}`; - await api.post(request_url); - return api.request(error.config); - } catch (error) { - console.error(error); - const router = useRouter(); - await router.push("/login"); - throw error; - } + (response) => { + return response; + }, + async (error) => { + if (error.response.status === 401 && !error.config._retried) { + // Set a flag to only try to retrieve an access token once, otherwise it keeps infinitely looping + error.config._retried = true; + const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}` + // Wait for the post response, to retrieve a new access token + await api.post(request_url, {}, error.config); + // Retry the request + return api.request(error.config); + } + // return an error if the response is an error and we already retried + return Promise.reject(error); } - return Promise.reject(error); - } ); -export default api; +export default api; \ No newline at end of file diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 89694c49..3c491713 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,5 +1,6 @@ function Home() { - return null; // instant redirect to login page + return null; // instant redirect to login page } export default Home; + diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 04a1d9c0..9af1f278 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -1,83 +1,59 @@ import BaseHeader from "@/components/header/BaseHeader"; -import styles from "styles/Login.module.css"; +import styles from "styles/Login.module.css" import Image from "next/image"; -import filler_logo from "../public/filler_logo.png"; +import filler_logo from "../public/filler_logo.png" import Link from "next/link"; -import login from "../lib/login"; -import { FormEvent, useContext, useState } from "react"; -import { useRouter } from "next/router"; -import AuthContext from "@/context/AuthProvider"; +import login from "../lib/login" +import {FormEvent, useState} from "react"; +import {useRouter} from "next/router"; export default function Login() { - let { loginUser } = useContext(AuthContext); - const router = useRouter(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); + const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); - const handleSubmit = async (event: FormEvent): Promise => { - event.preventDefault(); - try { - await login(username, password, router, loginUser); - } catch (error) { - console.error(error); - } - }; + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + try { + await login(username, password, router); + } catch (error) { + console.error(error); + } + }; - return ( - <> - -
-
- My App Logo -
-
-

Login.

-
- - ) => - setUsername(e.target.value) - } - /> - - ) => - setPassword(e.target.value) - } - required - /> - -
-

- - Forgot Password - -

-

- Don't have an account?{" "} - - Sign up here - -

-
-
- - ); -} + return ( + <> + +
+
+ My App Logo +
+
+

Login.

+
+ + ) => setUsername(e.target.value)} + /> + + ) => setPassword(e.target.value)} + required + /> + +
+

Forgot Password

+

Don't have an account? Sign up here +

+
+
+ + ) +} \ No newline at end of file diff --git a/frontend/pages/reset-password.tsx b/frontend/pages/reset-password.tsx index 8554d413..c41d9159 100644 --- a/frontend/pages/reset-password.tsx +++ b/frontend/pages/reset-password.tsx @@ -1,5 +1,5 @@ -import { useRouter } from "next/router"; -import React, { FormEvent, useState } from "react"; +import {useRouter} from "next/router"; +import React, {FormEvent, useState} from "react"; import BaseHeader from "@/components/header/BaseHeader"; import styles from "@/styles/Login.module.css"; import Image from "next/image"; @@ -8,62 +8,45 @@ import Link from "next/link"; import reset from "@/lib/reset"; export default function ResetPassword() { - const router = useRouter(); - const [email, setEmail] = useState(""); + const router = useRouter(); + const [email, setEmail] = useState(""); - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - try { - await reset(email, router); - } catch (error) { - console.error(error); + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + await reset(email, router); + } catch (error) { + console.error(error); + } } - }; - return ( - <> - -
-
- My App Logo -
-
-

- Enter your e-mail in order to find your account -

-
-
- - ) => - setEmail(e.target.value) - } - required - /> + return ( + <> + +
+
+ My App Logo +
+
+

Enter your e-mail in order to find your account

+
+ + + ) => setEmail(e.target.value)} + required/> - - -

- Already have an account?{" "} - - Log in here - -

-
-
- - ); -} + + +

Already have an account? Log in here +

+
+
+ + ); +} \ No newline at end of file diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 4e3599d8..35b95217 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -1,5 +1,5 @@ -import { useRouter } from "next/router"; -import React, { FormEvent, useState } from "react"; +import {useRouter} from "next/router"; +import React, {FormEvent, useState} from "react"; import BaseHeader from "@/components/header/BaseHeader"; import styles from "@/styles/Login.module.css"; import Image from "next/image"; @@ -7,124 +7,91 @@ import filler_logo from "@/public/filler_logo.png"; import Link from "next/link"; import signup from "@/lib/signup"; + export default function Signup() { - const router = useRouter(); - const [firstname, setFirstname] = useState(""); - const [lastname, setLastname] = useState(""); - const [email, setEmail] = useState(""); - const [password1, setPassword1] = useState(""); - const [password2, setPassword2] = useState(""); + const router = useRouter(); + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); + const [email, setEmail] = useState(""); + const [password1, setPassword1] = useState(""); + const [password2, setPassword2] = useState(""); + - const handleSubmit = async (event: FormEvent) => { - event.preventDefault(); - try { - await signup(firstname, lastname, email, password1, password2, router); - } catch (error) { - console.error(error); + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + try { + await signup(firstname, lastname, email, password1, password2, router); + } catch (error) { + console.error(error); + } } - }; - return ( - <> - -
-
- My App Logo -
-
-

Signup

-
- - ) => - setFirstname(e.target.value) - } - required - /> + return ( + <> + +
+
+ My App Logo +
+
+

Signup

+ + + ) => setFirstname(e.target.value)} + required/> - - ) => - setLastname(e.target.value) - } - required - /> + + ) => setLastname(e.target.value)} + required/> - - ) => - setEmail(e.target.value) - } - required - /> + + ) => setEmail(e.target.value)} + required/> - - ) => - setPassword1(e.target.value) - } - required - /> + + ) => setPassword1(e.target.value)} + required/> - - ) => - setPassword2(e.target.value) - } - required - /> + + ) => setPassword2(e.target.value)} + required/> - - -

- Already have an account?{" "} - - Log in here - -

-
-
- - ); -} + + +

Already have an account? Log in here +

+
+
+ + ); +} \ No newline at end of file diff --git a/frontend/pages/welcome.tsx b/frontend/pages/welcome.tsx index 9af7cd92..ed577cf0 100644 --- a/frontend/pages/welcome.tsx +++ b/frontend/pages/welcome.tsx @@ -2,80 +2,66 @@ import BaseHeader from "@/components/header/BaseHeader"; import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; -import api from "../pages/api/axios"; -import { useContext, useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import AuthContext from "@/context/AuthProvider"; +import api from "../pages/api/axios" +import {useEffect, useState} from "react"; +import {useRouter} from "next/router"; function Welcome() { - let { auth, logoutUser } = useContext(AuthContext); - const router = useRouter(); - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); // prevents preview welcome page before auth check + const router = useRouter(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); // prevents preview welcome page before auth check - useEffect(() => { - let present = "auth" in sessionStorage; - let value = sessionStorage.getItem("auth"); - if (present && value == "true") { - setLoading(false); - fetchData(); - } else { - router.push("/login"); - } - }, []); + useEffect(() => { + setData([]); + fetchData(); + }, []); - async function fetchData() { - try { - api - .get( - `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}` - ) - .then((info) => { - if (!info.data || info.data.length === 0) { - router.push("/login"); - } else { + async function fetchData() { + api.get(`${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}`).then(info => { + // Set loading to false only if the response is valid + setLoading(false); setData(info.data); - } + }, err => { + console.error(err); + if (err.response.status == 401) { + router.push('/login'); // Only redirect to login if the status code is 401: unauthorized + } }); - } catch (error) { - console.error(error); } - } - const handleLogout = async () => { - try { - const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); - if (response.status === 200) { - logoutUser(); - await router.push("/login"); - } - } catch (error) { - console.error(error); - } - }; + const handleLogout = async () => { + try { + const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); + if (response.status === 200) { + await router.push('/login'); + } + } catch (error) { + console.error(error); + } + }; - return ( - <> - {loading ? ( -
Loading...
- ) : ( + return ( <> - -

Welcome!

- Site coming soon - -

Users:

-
    - {data.map((item, index) => ( -
  • {JSON.stringify(item)}
  • - ))} -
+ {loading ? ( +
Loading...
+ ) : ( + <> + +

Welcome!

+ Site coming soon + +

Users:

+
    + {data.map((item, index) => ( +
  • {JSON.stringify(item)}
  • + ))} +
+ + )} - )} - - ); + ) + + } -export default Welcome; +export default Welcome \ No newline at end of file diff --git a/frontend/types.d.tsx b/frontend/types.d.tsx index 32968a26..d98e2945 100644 --- a/frontend/types.d.tsx +++ b/frontend/types.d.tsx @@ -1,16 +1,16 @@ export type Login = { - email: string; - password: string; -}; + email: string + password: string +} export type SignUp = { - first_name: string; - last_name: string; - email: string; - password1: string; - password2: string; -}; + first_name: string, + last_name: string, + email: string, + password1: string, + password2: string, +} export type Reset_Password = { - email: string; -}; + email: string +} From df6262efcd36e50c4291ffe91b438bb178130022 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Tue, 21 Mar 2023 00:36:46 +0100 Subject: [PATCH 0127/1000] API route for EmailWhitelist (#90) --- backend/config/urls.py | 2 + backend/email_whitelist/urls.py | 15 ++++ backend/email_whitelist/views.py | 122 ++++++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/backend/config/urls.py b/backend/config/urls.py index 46545274..ff3935d3 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -24,6 +24,7 @@ from building_comment import urls as building_comment_urls from building_on_tour import urls as building_on_tour_urls from email_template import urls as email_template_urls +from email_whitelist import urls as email_whitelist_urls from garbage_collection import urls as garbage_collection_urls from manual import urls as manual_urls from picture_building import urls as picture_building_urls @@ -44,6 +45,7 @@ path("building/", include(building_urls)), path("building_comment/", include(building_comment_urls)), path("email_template/", include(email_template_urls)), + path("email_whitelist/", include(email_whitelist_urls)), path("region/", include(region_urls)), path("garbage_collection/", include(garbage_collection_urls)), path("building_on_tour/", include(building_on_tour_urls)), diff --git a/backend/email_whitelist/urls.py b/backend/email_whitelist/urls.py index e69de29b..2d174b8e 100644 --- a/backend/email_whitelist/urls.py +++ b/backend/email_whitelist/urls.py @@ -0,0 +1,15 @@ +from django.urls import path + +from .views import ( + DefaultEmailWhiteList, + EmailWhiteListIndividualView, + EmailWhiteListAllView, + EmailWhiteListNewVerificationCode +) + +urlpatterns = [ + path("all/", EmailWhiteListAllView.as_view()), + path("new_verification_code//", EmailWhiteListNewVerificationCode.as_view()), + path("/", EmailWhiteListIndividualView.as_view()), + path("", DefaultEmailWhiteList.as_view()) +] diff --git a/backend/email_whitelist/views.py b/backend/email_whitelist/views.py index 91ea44a2..0ab5bd5b 100644 --- a/backend/email_whitelist/views.py +++ b/backend/email_whitelist/views.py @@ -1,3 +1,121 @@ -from django.shortcuts import render +from drf_spectacular.utils import extend_schema +from rest_framework.views import APIView -# Create your views here. +from base.models import EmailWhitelist +from base.serializers import EmailWhitelistSerializer +from util.request_response_util import * + + +def _add_verification_code_if_necessary(data): + if "verification_code" not in data: + data["verification_code"] = get_unique_uuid() + + +class DefaultEmailWhiteList(APIView): + serializer_class = EmailWhitelistSerializer + + @extend_schema( + description="If you do not set a verification_code yourself, the backend will provide a safe one for you.", + responses={201: EmailWhitelistSerializer, 400: None}) + def post(self, request): + """ + Create a new whitelisted email + """ + data = request_to_dict(request.data) + + _add_verification_code_if_necessary(data) + + email_whitelist_instance = EmailWhitelist() + + set_keys_of_instance(email_whitelist_instance, data) + + if r := try_full_clean_and_save(email_whitelist_instance): + return r + + return post_success(EmailWhitelistSerializer(email_whitelist_instance)) + + +class EmailWhiteListIndividualView(APIView): + serializer_class = EmailWhitelistSerializer + + @extend_schema(responses={200: EmailWhitelistSerializer, 400: None}) + def get(self, request, email_whitelist_id): + """ + Get info about an EmailWhitelist with given id + """ + email_whitelist_instance = EmailWhitelist.objects.filter(id=email_whitelist_id) + + if not email_whitelist_instance: + return bad_request("EmailWhitelist") + + return get_success(EmailWhitelistSerializer(email_whitelist_instance[0])) + + @extend_schema(responses={204: None, 400: None}) + def delete(self, request, email_whitelist_id): + """ + Patch EmailWhitelist with given id + """ + email_whitelist_instance = EmailWhitelist.objects.filter(id=email_whitelist_id) + + if not email_whitelist_instance: + return bad_request("EmailWhitelist") + + email_whitelist_instance[0].delete() + + return delete_success() + + @extend_schema(responses={204: None, 400: None}) + def patch(self, request, email_whitelist_id): + """ + Patch EmailWhitelist with given id + """ + email_whitelist_instance = EmailWhitelist.objects.filter(id=email_whitelist_id) + + if not email_whitelist_instance: + return bad_request("EmailWhitelist") + + email_whitelist_instance = email_whitelist_instance[0] + data = request_to_dict(request.data) + _add_verification_code_if_necessary(data) + + set_keys_of_instance(email_whitelist_instance) + + if r := try_full_clean_and_save(email_whitelist_instance): + return r + + return patch_success(EmailWhitelistSerializer(email_whitelist_instance)) + + +class EmailWhiteListNewVerificationCode(APIView): + serializer_class = EmailWhitelistSerializer + + @extend_schema(description="Generate a new token. The body of the request is ignored.", + responses={204: None, 400: None}) + def post(self, request, email_whitelist_id): + """ + Do a POST with an empty body on `email_whitelist/new_verification_code/ to generate a new verification code + """ + email_whitelist_instance = EmailWhitelist.objects.filter(id=email_whitelist_id) + + if not email_whitelist_instance: + return bad_request("EmailWhitelist") + + email_whitelist_instance = email_whitelist_instance[0] + email_whitelist_instance.verification_code = get_unique_uuid() + + if r := try_full_clean_and_save(email_whitelist_instance): + return r + + return post_success(EmailWhitelistSerializer(email_whitelist_instance)) + + +class EmailWhiteListAllView(APIView): + serializer_class = EmailWhitelistSerializer + + def get(self, request): + """ + Get info about the EmailWhiteList with given id + """ + email_whitelist_instances = EmailWhitelist.objects.all() + serializer = EmailWhitelistSerializer(email_whitelist_instances, many=True) + return get_success(serializer) From f938c9a16a292e524f573ee3675849859d9d42cf Mon Sep 17 00:00:00 2001 From: TiboStr Date: Mon, 20 Mar 2023 23:37:43 +0000 Subject: [PATCH 0128/1000] Auto formatted code --- backend/email_whitelist/urls.py | 4 ++-- backend/email_whitelist/views.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/email_whitelist/urls.py b/backend/email_whitelist/urls.py index 2d174b8e..60ee39f4 100644 --- a/backend/email_whitelist/urls.py +++ b/backend/email_whitelist/urls.py @@ -4,12 +4,12 @@ DefaultEmailWhiteList, EmailWhiteListIndividualView, EmailWhiteListAllView, - EmailWhiteListNewVerificationCode + EmailWhiteListNewVerificationCode, ) urlpatterns = [ path("all/", EmailWhiteListAllView.as_view()), path("new_verification_code//", EmailWhiteListNewVerificationCode.as_view()), path("/", EmailWhiteListIndividualView.as_view()), - path("", DefaultEmailWhiteList.as_view()) + path("", DefaultEmailWhiteList.as_view()), ] diff --git a/backend/email_whitelist/views.py b/backend/email_whitelist/views.py index 0ab5bd5b..349211d9 100644 --- a/backend/email_whitelist/views.py +++ b/backend/email_whitelist/views.py @@ -16,7 +16,8 @@ class DefaultEmailWhiteList(APIView): @extend_schema( description="If you do not set a verification_code yourself, the backend will provide a safe one for you.", - responses={201: EmailWhitelistSerializer, 400: None}) + responses={201: EmailWhitelistSerializer, 400: None}, + ) def post(self, request): """ Create a new whitelisted email @@ -89,8 +90,9 @@ def patch(self, request, email_whitelist_id): class EmailWhiteListNewVerificationCode(APIView): serializer_class = EmailWhitelistSerializer - @extend_schema(description="Generate a new token. The body of the request is ignored.", - responses={204: None, 400: None}) + @extend_schema( + description="Generate a new token. The body of the request is ignored.", responses={204: None, 400: None} + ) def post(self, request, email_whitelist_id): """ Do a POST with an empty body on `email_whitelist/new_verification_code/ to generate a new verification code From 09c4bc02984277d60eb12c599aa8c590862ac094 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Mon, 20 Mar 2023 23:42:17 +0000 Subject: [PATCH 0129/1000] Auto formatted code --- frontend/components/header/BaseHeader.tsx | 17 +++---- frontend/lib/login.tsx | 13 +++-- frontend/lib/reset.tsx | 17 +++---- frontend/lib/signup.tsx | 18 ++++--- frontend/next.config.js | 22 ++++----- frontend/pages/_app.tsx | 10 ++-- frontend/pages/_document.tsx | 10 ++-- frontend/pages/api/axios.tsx | 8 +-- frontend/pages/index.tsx | 1 - frontend/pages/login.tsx | 43 +++++++++++------ frontend/pages/reset-password.tsx | 31 +++++++----- frontend/pages/signup.tsx | 59 +++++++++++++++-------- frontend/pages/welcome.tsx | 43 +++++++++-------- frontend/types.d.tsx | 22 ++++----- 14 files changed, 177 insertions(+), 137 deletions(-) diff --git a/frontend/components/header/BaseHeader.tsx b/frontend/components/header/BaseHeader.tsx index be3f05ce..ca3b2aef 100644 --- a/frontend/components/header/BaseHeader.tsx +++ b/frontend/components/header/BaseHeader.tsx @@ -1,21 +1,16 @@ -import React from 'react'; -import Image from 'next/image' -import logo from '../../public/logo.png' -import styles from './BaseHeader.module.css'; +import React from "react"; +import Image from "next/image"; +import logo from "../../public/logo.png"; +import styles from "./BaseHeader.module.css"; const BaseHeader = () => { return (
- My App Logo + My App Logo
); }; -export default BaseHeader; \ No newline at end of file +export default BaseHeader; diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index 86f7eaf5..d1b0c4f1 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -1,9 +1,8 @@ -import api from '../pages/api/axios'; -import {Login} from "@/types.d"; -import {NextRouter} from "next/router"; +import api from "../pages/api/axios"; +import { Login } from "@/types.d"; +import { NextRouter } from "next/router"; const login = async (email: string, password: string, router: NextRouter): Promise => { - const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; const login_data: Login = { email: email, @@ -12,11 +11,11 @@ const login = async (email: string, password: string, router: NextRouter): Promi // Attempt to login with axios so authentication tokens get saved in our axios instance api.post(host, login_data, { - headers: {'Content-Type': 'application/json'} + headers: { "Content-Type": "application/json" }, }) .then((response: { status: number }) => { if (response.status == 200) { - router.push('/welcome'); + router.push("/welcome"); } }) .catch((error) => { @@ -24,4 +23,4 @@ const login = async (email: string, password: string, router: NextRouter): Promi }); }; -export default login; \ No newline at end of file +export default login; diff --git a/frontend/lib/reset.tsx b/frontend/lib/reset.tsx index f4feb160..1e923048 100644 --- a/frontend/lib/reset.tsx +++ b/frontend/lib/reset.tsx @@ -1,28 +1,27 @@ -import {Reset_Password} from "@/types.d"; -import {NextRouter} from "next/router"; +import { Reset_Password } from "@/types.d"; +import { NextRouter } from "next/router"; const reset = async (email: string, router: NextRouter): Promise => { - - const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}` + const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_RESET_PASSWORD}`; const reset_data: Reset_Password = { email: email, - } + }; try { // Request without axios because no authentication is needed for this POST request const response = await fetch(host, { method: "POST", - headers: {"Content-Type": "application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(reset_data), }); if (response.status == 200) { alert("A password reset e-mail has been sent to the provided e-mail address"); - await router.push('/login'); + await router.push("/login"); } } catch (error) { console.error(error); } -} +}; -export default reset; \ No newline at end of file +export default reset; diff --git a/frontend/lib/signup.tsx b/frontend/lib/signup.tsx index b036cc1b..5c259272 100644 --- a/frontend/lib/signup.tsx +++ b/frontend/lib/signup.tsx @@ -1,7 +1,13 @@ -import {SignUp} from "@/types.d"; - -const signup = async (firstname: string, lastname: string, email: string, password1: string, password2: string, router: any): Promise => { +import { SignUp } from "@/types.d"; +const signup = async ( + firstname: string, + lastname: string, + email: string, + password1: string, + password2: string, + router: any +): Promise => { const host = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_SIGNUP}`; const signup_data: SignUp = { first_name: firstname, @@ -9,7 +15,7 @@ const signup = async (firstname: string, lastname: string, email: string, passwo email: email, password1: password1, password2: password2, - } + }; // TODO Display error message from backend that will check this // Small check if passwords are equal @@ -22,7 +28,7 @@ const signup = async (firstname: string, lastname: string, email: string, passwo // Request without axios because no authentication is needed for this POST request const response = await fetch(host, { method: "POST", - headers: {"Content-Type": "application/json"}, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(signup_data), }); if (response.status == 201) { @@ -32,6 +38,6 @@ const signup = async (firstname: string, lastname: string, email: string, passwo } catch (error) { console.error(error); } -} +}; export default signup; diff --git a/frontend/next.config.js b/frontend/next.config.js index 03ab1813..4a002860 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,15 +1,15 @@ /** @type {{redirects(): Promise<[{permanent: boolean, destination: string, source: string}]>}} */ const nextConfig = { - async redirects() { - return [ - { - source: '/', - destination: '/login', - permanent: true, - }, - ] - } -} + async redirects() { + return [ + { + source: "/", + destination: "/login", + permanent: true, + }, + ]; + }, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 94252577..4fa1d952 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,8 +1,6 @@ -import '@/styles/globals.css' -import type {AppProps} from 'next/app' +import "@/styles/globals.css"; +import type { AppProps } from "next/app"; -export default function App({Component, pageProps}: AppProps) { - return ( - - ); +export default function App({ Component, pageProps }: AppProps) { + return ; } diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx index 81f4832d..2f4bae41 100644 --- a/frontend/pages/_document.tsx +++ b/frontend/pages/_document.tsx @@ -1,13 +1,13 @@ -import {Html, Head, Main, NextScript} from 'next/document' +import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( - + -
- +
+ - ) + ); } diff --git a/frontend/pages/api/axios.tsx b/frontend/pages/api/axios.tsx index 3c5d62ba..7340434c 100644 --- a/frontend/pages/api/axios.tsx +++ b/frontend/pages/api/axios.tsx @@ -1,10 +1,10 @@ -import axios from 'axios'; +import axios from "axios"; axios.defaults.withCredentials = true; // Instance used to make authenticated requests const api = axios.create({ baseURL: `${process.env.NEXT_PUBLIC_BASE_API_URL}`, - withCredentials: true + withCredentials: true, }); // Intercept on response and renew refresh token if necessary @@ -16,7 +16,7 @@ api.interceptors.response.use( if (error.response.status === 401 && !error.config._retried) { // Set a flag to only try to retrieve an access token once, otherwise it keeps infinitely looping error.config._retried = true; - const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}` + const request_url: string = `${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}`; // Wait for the post response, to retrieve a new access token await api.post(request_url, {}, error.config); // Retry the request @@ -27,4 +27,4 @@ api.interceptors.response.use( } ); -export default api; \ No newline at end of file +export default api; diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 3c491713..80022e33 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -3,4 +3,3 @@ function Home() { } export default Home; - diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 9af1f278..746365ed 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -1,14 +1,13 @@ import BaseHeader from "@/components/header/BaseHeader"; -import styles from "styles/Login.module.css" +import styles from "styles/Login.module.css"; import Image from "next/image"; -import filler_logo from "../public/filler_logo.png" +import filler_logo from "../public/filler_logo.png"; import Link from "next/link"; -import login from "../lib/login" -import {FormEvent, useState} from "react"; -import {useRouter} from "next/router"; +import login from "../lib/login"; +import { FormEvent, useState } from "react"; +import { useRouter } from "next/router"; export default function Login() { - const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -24,22 +23,26 @@ export default function Login() { return ( <> - +
- My App Logo + My App Logo

Login.

- + ) => setUsername(e.target.value)} /> - + ) => setPassword(e.target.value)} required /> - +
-

Forgot Password

-

Don't have an account? Sign up here +

+ + Forgot Password + +

+

+ Don't have an account?{" "} + + Sign up here +

- ) -} \ No newline at end of file + ); +} diff --git a/frontend/pages/reset-password.tsx b/frontend/pages/reset-password.tsx index c41d9159..4c3b89b2 100644 --- a/frontend/pages/reset-password.tsx +++ b/frontend/pages/reset-password.tsx @@ -1,5 +1,5 @@ -import {useRouter} from "next/router"; -import React, {FormEvent, useState} from "react"; +import { useRouter } from "next/router"; +import React, { FormEvent, useState } from "react"; import BaseHeader from "@/components/header/BaseHeader"; import styles from "@/styles/Login.module.css"; import Image from "next/image"; @@ -18,20 +18,22 @@ export default function ResetPassword() { } catch (error) { console.error(error); } - } + }; return ( <> - +
- My App Logo + My App Logo

Enter your e-mail in order to find your account

-
+
- + ) => setEmail(e.target.value)} - required/> + required + /> - +
-

Already have an account? Log in here +

+ Already have an account?{" "} + + Log in here +

); -} \ No newline at end of file +} diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 35b95217..82eea0cf 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -1,5 +1,5 @@ -import {useRouter} from "next/router"; -import React, {FormEvent, useState} from "react"; +import { useRouter } from "next/router"; +import React, { FormEvent, useState } from "react"; import BaseHeader from "@/components/header/BaseHeader"; import styles from "@/styles/Login.module.css"; import Image from "next/image"; @@ -7,7 +7,6 @@ import filler_logo from "@/public/filler_logo.png"; import Link from "next/link"; import signup from "@/lib/signup"; - export default function Signup() { const router = useRouter(); const [firstname, setFirstname] = useState(""); @@ -16,7 +15,6 @@ export default function Signup() { const [password1, setPassword1] = useState(""); const [password2, setPassword2] = useState(""); - const handleSubmit = async (event: FormEvent) => { event.preventDefault(); try { @@ -24,19 +22,21 @@ export default function Signup() { } catch (error) { console.error(error); } - } + }; return ( <> - +
- My App Logo + My App Logo

Signup

- + ) => setFirstname(e.target.value)} - required/> + required + /> - + ) => setLastname(e.target.value)} - required/> + required + /> - + ) => setEmail(e.target.value)} - required/> + required + /> - + ) => setPassword1(e.target.value)} - required/> + required + /> - + ) => setPassword2(e.target.value)} - required/> + required + /> - +
-

Already have an account? Log in here +

+ Already have an account?{" "} + + Log in here +

); -} \ No newline at end of file +} diff --git a/frontend/pages/welcome.tsx b/frontend/pages/welcome.tsx index ed577cf0..fe5d78d8 100644 --- a/frontend/pages/welcome.tsx +++ b/frontend/pages/welcome.tsx @@ -2,9 +2,9 @@ import BaseHeader from "@/components/header/BaseHeader"; import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; -import api from "../pages/api/axios" -import {useEffect, useState} from "react"; -import {useRouter} from "next/router"; +import api from "../pages/api/axios"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; function Welcome() { const router = useRouter(); @@ -17,23 +17,26 @@ function Welcome() { }, []); async function fetchData() { - api.get(`${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}`).then(info => { - // Set loading to false only if the response is valid - setLoading(false); - setData(info.data); - }, err => { - console.error(err); - if (err.response.status == 401) { - router.push('/login'); // Only redirect to login if the status code is 401: unauthorized + api.get(`${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}`).then( + (info) => { + // Set loading to false only if the response is valid + setLoading(false); + setData(info.data); + }, + (err) => { + console.error(err); + if (err.response.status == 401) { + router.push("/login"); // Only redirect to login if the status code is 401: unauthorized + } } - }); + ); } const handleLogout = async () => { try { const response = await api.post(`${process.env.NEXT_PUBLIC_API_LOGOUT}`); if (response.status === 200) { - await router.push('/login'); + await router.push("/login"); } } catch (error) { console.error(error); @@ -46,10 +49,12 @@ function Welcome() {
Loading...
) : ( <> - +

Welcome!

- Site coming soon - + Site coming soon +

Users:

There are currently 22 users, all of whom share the same password `drtrottoir123`. From d51db363d65633b11d6aadd7444007c2a7e3e101 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 14:07:23 +0100 Subject: [PATCH 0212/1000] #106 added eer --- docs/db/ER_diagram.jpg | Bin 0 -> 183531 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/db/ER_diagram.jpg diff --git a/docs/db/ER_diagram.jpg b/docs/db/ER_diagram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..218095c54c30df38f3e24fc95c25a16a4306dc24 GIT binary patch literal 183531 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�QX`p;!+vX9e!atBB(0d{v+9xC<=u=`Y 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

+ setEditBuilding(false)}> + + Bewerk gebouw + + + + + Stad + + + + Public id + + + De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link TODO + + + + + + + + + + + +

- Gebouw + Gebouw { + e.preventDefault(); + setEditBuilding(true); + }}>

Naam: {building?.name}

Stad: {building?.city}

@@ -71,15 +112,15 @@ function SyndicDashboard() {

Nr: {building?.house_number}

Bus: {building?.bus}

Client id: {building?.client_id}

-

Duration: {building?.duration}

+

Duration: {"" + building?.duration}

Region: {building?.region_id}

Public id: {building?.public_id}

https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1310&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486

- Site coming soon - + Site coming soon + ); } From a84f20c03ec14305f3497bc0a41155119a74b607 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Tue, 28 Mar 2023 09:31:59 +0000 Subject: [PATCH 0357/1000] Auto formatted code --- frontend/pages/syndic/dashboard.tsx | 59 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index 9fccacd5..4bf071b4 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -3,17 +3,15 @@ import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; import LogoutButton from "@/components/logoutbutton"; -import {withAuthorisation} from "@/components/withAuthorisation"; -import {useRouter} from "next/router"; -import {TiPencil} from "react-icons/ti"; -import {BuildingInterface, getBuildingInfo, getBuildingsFromOwner} from "@/lib/building"; -import {useEffect, useState} from "react"; -import {AxiosResponse} from "axios/index"; -import {Button, Form, Modal} from 'react-bootstrap'; +import { withAuthorisation } from "@/components/withAuthorisation"; +import { useRouter } from "next/router"; +import { TiPencil } from "react-icons/ti"; +import { BuildingInterface, getBuildingInfo, getBuildingsFromOwner } from "@/lib/building"; +import { useEffect, useState } from "react"; +import { AxiosResponse } from "axios/index"; +import { Button, Form, Modal } from "react-bootstrap"; - -interface ParsedUrlQuery { -} +interface ParsedUrlQuery {} interface DashboardQuery extends ParsedUrlQuery { id?: string; @@ -46,7 +44,7 @@ function SyndicDashboard() { return ( <> - +
Welcome to the Syndic Dashboard! setEditBuilding(false)}> - - Bewerk gebouw - + Bewerk gebouw
Stad - - + Public id - + De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link TODO - - -
-

- Gebouw { - e.preventDefault(); - setEditBuilding(true); - }}> + Gebouw{" "} + { + e.preventDefault(); + setEditBuilding(true); + }} + >

Naam: {building?.name}

Stad: {building?.city}

@@ -119,8 +116,8 @@ function SyndicDashboard() {

https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1310&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486

- Site coming soon - + Site coming soon + ); } From 7ec678c0adec2343e6644f7b95ccb2f268323adb Mon Sep 17 00:00:00 2001 From: Emma N Date: Tue, 28 Mar 2023 12:04:58 +0200 Subject: [PATCH 0358/1000] header with filters --- .../components/header/AdminFilterHeader.tsx | 112 ++++++++++++++++++ .../components/header/RoleHeader.module.css | 16 +++ 2 files changed, 128 insertions(+) create mode 100644 frontend/components/header/AdminFilterHeader.tsx diff --git a/frontend/components/header/AdminFilterHeader.tsx b/frontend/components/header/AdminFilterHeader.tsx new file mode 100644 index 00000000..0a216c1b --- /dev/null +++ b/frontend/components/header/AdminFilterHeader.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import styles from "@/components/header/RoleHeader.module.css"; +import Image from "next/image"; +import logo from "@/public/logo.png"; +import person from "@/public/icons/person.svg"; +import menu from "@/public/icons/menu.svg"; + +const AdminFilterHeader = () => { + return ( +
+ + ); +}; + +export default AdminFilterHeader; \ No newline at end of file diff --git a/frontend/components/header/RoleHeader.module.css b/frontend/components/header/RoleHeader.module.css index cd40f9fe..a65bb99d 100644 --- a/frontend/components/header/RoleHeader.module.css +++ b/frontend/components/header/RoleHeader.module.css @@ -26,3 +26,19 @@ .button:hover { color: transparent; } + +.grid { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: 1fr; +} + +.toplevel { + grid-template-columns: auto auto; +} + +.input { + max-width: 350px; + padding: 5px; + margin: 5px; +} From 8f1458062e4d9c49481e9abe7d8f67cbcf8e48e1 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 17:26:15 +0200 Subject: [PATCH 0359/1000] remove unnecessary import --- frontend/lib/login.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index d13dadbd..c11a9fa0 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -1,7 +1,6 @@ import api from "./api/axios"; import {Login} from "@/types.d"; import {AxiosResponse} from "axios"; -import * as process from "process"; export const login = (email: string, password: string): Promise> => { const host: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGIN}`; From fd69752bd9c1c2879ddf77d385ec71aa24df43f7 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Tue, 28 Mar 2023 19:52:16 +0200 Subject: [PATCH 0360/1000] fix pk sites in datadump --- backend/datadump.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/datadump.json b/backend/datadump.json index 2e1da9b2..99b2ab88 100644 --- a/backend/datadump.json +++ b/backend/datadump.json @@ -1,6 +1,7 @@ [ { "model": "sites.site", + "pk": 2, "fields": { "domain": "localhost", "name": "localhost" @@ -8,6 +9,7 @@ }, { "model": "sites.site", + "pk": 3, "fields": { "domain": "sel2-4.ugent.be", "name": "sel2-4.ugent.be" From 9d1f712b43337f2d50e7fea4da79ceb78f23d175 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Tue, 28 Mar 2023 19:57:40 +0200 Subject: [PATCH 0361/1000] set ids to 1 and 2 --- backend/config/settings.py | 2 +- backend/datadump.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/config/settings.py b/backend/config/settings.py index 234ba4aa..6d99268c 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -123,7 +123,7 @@ ACCOUNT_EMAIL_VERIFICATION = None LOGIN_URL = "http://localhost/api/authentication/login" -SITE_ID = 2 if DEBUG else 3 +SITE_ID = 1 if DEBUG else 2 MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", diff --git a/backend/datadump.json b/backend/datadump.json index 99b2ab88..879b80a5 100644 --- a/backend/datadump.json +++ b/backend/datadump.json @@ -1,7 +1,7 @@ [ { "model": "sites.site", - "pk": 2, + "pk": 1, "fields": { "domain": "localhost", "name": "localhost" @@ -9,7 +9,7 @@ }, { "model": "sites.site", - "pk": 3, + "pk": 2, "fields": { "domain": "sel2-4.ugent.be", "name": "sel2-4.ugent.be" From 2a6c1df537fc934c6391e44b670ebe0a4fe984e4 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Tue, 28 Mar 2023 17:58:28 +0000 Subject: [PATCH 0362/1000] Auto formatted code --- backend/config/settings.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/config/settings.py b/backend/config/settings.py index 6d99268c..7d13ff0f 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -38,7 +38,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.sites", - 'django_nose', + "django_nose", ] AUTHENTICATION = [ @@ -75,7 +75,7 @@ # https://stackoverflow.com/a/70641487 collections.Callable = collections.abc.Callable # Use nose to run all tests -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +TEST_RUNNER = "django_nose.NoseTestSuiteRunner" # drf-spectacular settings SPECTACULAR_SETTINGS = { @@ -98,7 +98,7 @@ "JWT_AUTH_REFRESH_COOKIE": "auth-refresh-token", "USER_DETAILS_SERIALIZER": "base.serializers.UserSerializer", "PASSWORD_RESET_SERIALIZER": "authentication.serializers.CustomPasswordResetSerializer", - "PASSWORD_RESET_USE_SITES_DOMAIN": True + "PASSWORD_RESET_USE_SITES_DOMAIN": True, } SIMPLE_JWT = { @@ -176,16 +176,16 @@ # https://docs.djangoproject.com/en/4.1/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'drtrottoir', - 'USER': 'django', - 'PASSWORD': 'password', + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "drtrottoir", + "USER": "django", + "PASSWORD": "password", # since testing is run outside the docker, we need a localhost db # the postgres docker port is exposed to it should be used as well # this 'hack' is just to fix the name resolving of 'web' - 'HOST': 'localhost' if "test" in sys.argv else "web", - 'PORT': '5432', + "HOST": "localhost" if "test" in sys.argv else "web", + "PORT": "5432", } } From 044a72ff1c600234a743daa0247acbe45c0de2aa Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 20:31:04 +0200 Subject: [PATCH 0363/1000] remove unnecessary comment --- backend/base/models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index e43be0f1..eabf57da 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -23,13 +23,6 @@ def __str__(self): return self.region -# Catches the post_save signal (in signals.py) and creates a user token if not yet created -# @receiver(post_save, sender=settings.AUTH_USER_MODEL) -# def create_auth_token(sender, instance=None, created=False, **kwargs): -# if created: -# Token.objects.create(user=instance) - - class Role(models.Model): name = models.CharField(max_length=20) rank = models.PositiveIntegerField() From ee09b9226656017cdfebd591ad9420fdfe3ad036 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Tue, 28 Mar 2023 21:28:33 +0200 Subject: [PATCH 0364/1000] Swapped syndig building and dashboard (#188 & #189) --- frontend/pages/syndic/building.tsx | 169 +++++++++++++++++----------- frontend/pages/syndic/dashboard.tsx | 162 ++++++++++---------------- 2 files changed, 165 insertions(+), 166 deletions(-) diff --git a/frontend/pages/syndic/building.tsx b/frontend/pages/syndic/building.tsx index 7a48cba9..ffd9acf5 100644 --- a/frontend/pages/syndic/building.tsx +++ b/frontend/pages/syndic/building.tsx @@ -1,88 +1,125 @@ import BaseHeader from "@/components/header/BaseHeader"; -import { BuildingInterface, getBuildingsFromOwner } from "@/lib/building"; -import router from "next/router"; -import { useEffect, useState } from "react"; -import { withAuthorisation } from "@/components/withAuthorisation"; -import SyndicDashboard from "@/pages/syndic/dashboard"; -import { AxiosResponse } from "axios"; +import {BuildingInterface, getBuildingInfo} from "@/lib/building"; +import {useRouter} from "next/router"; +import {useEffect, useState} from "react"; +import {withAuthorisation} from "@/components/withAuthorisation"; +import {AxiosResponse} from "axios"; +import styles from "@/styles/Welcome.module.css"; +import {Button, Form, Modal} from "react-bootstrap"; +import {TiPencil} from "react-icons/ti"; +import Image from "next/image"; +import soon from "@/public/coming_soon.png"; +import LogoutButton from "@/components/logoutbutton"; + +interface ParsedUrlQuery { +} + +interface DashboardQuery extends ParsedUrlQuery { + id?: string; +} function SyndicBuilding() { - const [id, setId] = useState(""); - const [buildings, setBuildings] = useState([]); + const router = useRouter(); + const query = router.query as DashboardQuery; - useEffect(() => { - setId(sessionStorage.getItem("id") || ""); - }, []); + const [building, setBuilding] = useState(null); + const [editBuilding, setEditBuilding] = useState(false); useEffect(() => { - console.log("hier"); - - if (!id) { - console.log(`nog geen id ${id}`); + if (!query.id) { return; } - console.log(`id is ${id}`); - - async function fetchBuildings() { - getBuildingsFromOwner(id) + async function fetchBuilding() { + getBuildingInfo(query.id) .then((buildings: AxiosResponse) => { - setBuildings(buildings.data); + setBuilding(buildings.data); }) .catch((error) => { console.log(error); }); } - fetchBuildings(); - }, [id]); - - if (!id || !buildings) { - //TODO: loading component, how to use? Maybe with a wrappen and a state boolean? - return
loading...
; - } + fetchBuilding(); + }, [query.id]); return ( <> - <> - - -

- https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1145&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 -

- -

Uw gebouwen

- -
- {buildings.map((building: BuildingInterface) => { - // console.log(building); - return ( -
{ - e.preventDefault(); - router.push({ - pathname: "dashboard", - query: { id: building.id }, - }); - }} - > -
-
-
- {building.name} {building.postal_code} {building.city} -
-

- {building.street} {building.house_number}{" "} -

-
-
-
- ); - })} -
- + + + + + {JSON.stringify(building)} + +

Welcome to the Syndic Dashboard!

+ + setEditBuilding(false)}> + + Bewerk gebouw + + +
+ + Stad + + + + Public id + + + De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link TODO + + + + + + + +
+
+
+ + +

+ Gebouw { + e.preventDefault(); + setEditBuilding(true); + }}> +

+

Naam: {building?.name}

+

Stad: {building?.city}

+

Postcode: {building?.postal_code}

+

Straat: {building?.street}

+

Nr: {building?.house_number}

+

Bus: {building?.bus}

+

Client id: {building?.client_id}

+

Duration: {"" + building?.duration}

+

Region: {building?.region_id}

+

Public id: {building?.public_id}

+ +

+ https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1310&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 +

+ Site coming soon + ); } diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index 9fccacd5..fe51ab96 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -1,126 +1,88 @@ import BaseHeader from "@/components/header/BaseHeader"; -import styles from "styles/Welcome.module.css"; -import soon from "public/coming_soon.png"; -import Image from "next/image"; -import LogoutButton from "@/components/logoutbutton"; import {withAuthorisation} from "@/components/withAuthorisation"; -import {useRouter} from "next/router"; -import {TiPencil} from "react-icons/ti"; -import {BuildingInterface, getBuildingInfo, getBuildingsFromOwner} from "@/lib/building"; +import router from "next/router"; +import {BuildingInterface, getBuildingsFromOwner} from "@/lib/building"; import {useEffect, useState} from "react"; import {AxiosResponse} from "axios/index"; -import {Button, Form, Modal} from 'react-bootstrap'; -interface ParsedUrlQuery { -} - -interface DashboardQuery extends ParsedUrlQuery { - id?: string; -} - function SyndicDashboard() { - const router = useRouter(); - const query = router.query as DashboardQuery; + const [id, setId] = useState(""); + const [buildings, setBuildings] = useState([]); - const [building, setBuilding] = useState(null); - const [editBuilding, setEditBuilding] = useState(false); + useEffect(() => { + setId(sessionStorage.getItem("id") || ""); + }, []); useEffect(() => { - if (!query.id) { + console.log("hier"); + + if (!id) { + console.log(`nog geen id ${id}`); return; } - async function fetchBuilding() { - getBuildingInfo(query.id) + console.log(`id is ${id}`); + + async function fetchBuildings() { + getBuildingsFromOwner(id) .then((buildings: AxiosResponse) => { - setBuilding(buildings.data); + setBuildings(buildings.data); }) .catch((error) => { console.log(error); }); } - fetchBuilding(); - }, [query.id]); + fetchBuildings(); + }, [id]); + + if (!id || !buildings) { + //TODO: loading component, how to use? Maybe with a wrappen and a state boolean? + return
loading...
; + } return ( <> - - - - - {JSON.stringify(building)} - -

Welcome to the Syndic Dashboard!

- - setEditBuilding(false)}> - - Bewerk gebouw - - -
- - Stad - - - - Public id - - - De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link TODO - - - - - - - -
-
-
- - -

- Gebouw { - e.preventDefault(); - setEditBuilding(true); - }}> -

-

Naam: {building?.name}

-

Stad: {building?.city}

-

Postcode: {building?.postal_code}

-

Straat: {building?.street}

-

Nr: {building?.house_number}

-

Bus: {building?.bus}

-

Client id: {building?.client_id}

-

Duration: {"" + building?.duration}

-

Region: {building?.region_id}

-

Public id: {building?.public_id}

- -

- https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1310&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 -

- Site coming soon - + <> + + +

+ https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1145&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 +

+ +

Uw gebouwen

+ +
+ {buildings.map((building: BuildingInterface) => { + // console.log(building); + return ( +
{ + e.preventDefault(); + router.push({ + pathname: "building", + query: {id: building.id}, + }); + }} + > +
+
+
+ {building.name} {building.postal_code} {building.city} +
+

+ {building.street} {building.house_number}{" "} +

+
+
+
+ ); + })} +
+ ); } From 27c02291bfd6e0f1aedf259c4e36be608242577d Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 22:09:45 +0200 Subject: [PATCH 0365/1000] reworked signup serializer and view --- backend/authentication/serializers.py | 125 +++++++++++++++++++------- backend/authentication/urls.py | 5 +- backend/authentication/views.py | 77 ++++------------ 3 files changed, 108 insertions(+), 99 deletions(-) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 3f1ab795..2c93aa96 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,44 +1,86 @@ -from dj_rest_auth import serializers -from dj_rest_auth.registration.serializers import RegisterSerializer +from allauth.account.adapter import get_adapter +from allauth.utils import email_address_exists +from dj_rest_auth import serializers as auth_serializers from dj_rest_auth.serializers import PasswordResetSerializer from django.utils.translation import gettext_lazy as _ +from phonenumber_field.serializerfields import PhoneNumberField +from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.serializers import Serializer from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken from rest_framework_simplejwt.tokens import RefreshToken from authentication.forms import CustomAllAuthPasswordResetForm -from base.models import User +from base.models import User, Lobby from config import settings -from users.user_utils import add_regions_to_user -from util.request_response_util import request_to_dict - - -class CustomRegisterSerializer(RegisterSerializer): - def update(self, instance, validated_data): - return super().update(instance, validated_data) - - def create(self, validated_data): - return super().create(validated_data) - - def custom_signup(self, request, user: User): - data = request_to_dict(request.data) - user.first_name = data.get("first_name") - user.last_name = data.get("last_name") - user.phone_number = data.get("phone_number") - user.role_id = data.get("role") - raw_regions = data.get("region") - if raw_regions: - add_regions_to_user(user, raw_regions) - user.save() +from users.views import TRANSLATE +from util.request_response_util import set_keys_of_instance, try_full_clean_and_save + + +class SignupSerializer(Serializer): + email = serializers.EmailField(required=True) + first_name = serializers.CharField(required=True) + last_name = serializers.CharField(required=True) + phone_number = PhoneNumberField(required=True) + password1 = serializers.CharField(required=True, write_only=True) + password2 = serializers.CharField(required=True, write_only=True) + verification_code = serializers.CharField(required=True, write_only=True) + + def validate_password1(self, password): + return get_adapter().clean_password(password) + + def validate_email(self, email): + email = get_adapter().clean_email(email) + if email and email_address_exists(email): + raise serializers.ValidationError( + _("a user is already registered with this e-mail address"), + ) + return email + + def validate(self, data): + # check if the email address is in the lobby + lobby_instance = Lobby.objects.filter(email=data["email"]).first() + if not lobby_instance: + raise auth_serializers.ValidationError({ + "email": + _(f"{data['email']} has no entry in the lobby, you must contact an admin to gain access to the platform"), + }) + # check if the verification code is valid + if lobby_instance.verification_code != data["verification_code"]: + raise auth_serializers.ValidationError({ + "verification_code": + _(f"invalid verification code") + }) + # add role to the validated data + data["role"] = lobby_instance.role_id + # check if passwords match + if data["password1"] != data["password2"]: + raise serializers.ValidationError({ + "message": + _("the two password fields didn't match.") + }) + # add password to the validated data + data["password"] = data["password1"] + + return data + + def save(self, data): + + user_instance = User() + + set_keys_of_instance(user_instance, data, TRANSLATE) + + if r := try_full_clean_and_save(user_instance): + raise auth_serializers.ValidationError( + r.data + ) + + user_instance.save() + + return user_instance class CustomTokenRefreshSerializer(Serializer): - def update(self, instance, validated_data): - return super().update(instance, validated_data) - - def create(self, validated_data): - return super().create(validated_data) def validate(self, incoming_data): # extract the request @@ -71,11 +113,6 @@ def validate(self, incoming_data): class CustomTokenVerifySerializer(Serializer): - def update(self, instance, validated_data): - return super().update(instance, validated_data) - - def create(self, validated_data): - return super().create(validated_data) def validate(self, incoming_data): # extract the request @@ -106,3 +143,23 @@ def validate_email(self, value): raise serializers.ValidationError(self.reset_form.errors) return value + + +class LobbyVerificationSerializer(Serializer): + email = serializers.EmailField() + verification_code = serializers.CharField() + + def validate_email(self, email): + lobby_instances = Lobby.objects.filter(email=email) + if not lobby_instances: + raise auth_serializers.ValidationError( + f"{email} has no entry in the lobby, you must contact an admin to gain access to the platform", + code='no_entry_in_lobby' + ) + return email + + def validate_verification_code(self, code): + lobby_instance = Lobby.objects.filter(verification_code=code).first() + if lobby_instance and lobby_instance.verification_code != code: + raise auth_serializers.ValidationError("invalid verification code", code='invalid_verification_code') + return code diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 874b3c9c..61bfb9a4 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -5,17 +5,16 @@ from django.urls import path from authentication.views import ( - CustomSignupView, CustomLoginView, CustomTokenVerifyView, CustomTokenRefreshView, CustomLogoutView, - CustomPasswordChangeView, + CustomPasswordChangeView, SignupView, ) urlpatterns = [ # URLs that do not require a session or valid token - path("signup/", CustomSignupView.as_view()), + path("signup/", SignupView.as_view()), path("password/reset/", PasswordResetView.as_view(), name="password_reset"), path("password/reset/confirm///", PasswordResetConfirmView.as_view(), name="password_reset_confirm"), path("login/", CustomLoginView.as_view()), diff --git a/backend/authentication/views.py b/backend/authentication/views.py index cdd3e666..70412962 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -3,7 +3,6 @@ set_jwt_access_cookie, set_jwt_refresh_cookie, set_jwt_cookies, ) -from dj_rest_auth.utils import jwt_encode from dj_rest_auth.views import LoginView, PasswordChangeView from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema @@ -15,76 +14,30 @@ from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView -from authentication.serializers import CustomRegisterSerializer, CustomTokenRefreshSerializer, \ - CustomTokenVerifySerializer +from authentication.serializers import CustomTokenRefreshSerializer, \ + CustomTokenVerifySerializer, SignupSerializer from base.models import Lobby from base.serializers import UserSerializer from config import settings -from util.request_response_util import request_to_dict +from util.request_response_util import post_success, post_docs -class CustomSignupView(APIView): - serializer_class = CustomRegisterSerializer - - @extend_schema(responses={200: None, 403: None}) +class SignupView(APIView): + @extend_schema(post_docs(SignupSerializer)) def post(self, request): """ Register a new user """ - data = request_to_dict(request.data) - - required_keys = [ - "email", - "verification_code", - "password1", - "password2", - "phone_number", - "first_name", - "last_name" - ] - missing_keys = [k for k in required_keys if k not in data.keys()] - if missing_keys: - return Response( - {k: ["This field is required."] for k in missing_keys}, - status=status.HTTP_400_BAD_REQUEST - ) - # check if there is a lobby entry for this email address - lobby_instances = Lobby.objects.filter(email=data.get('email')) - if not lobby_instances: - return Response( - { - "message": f"{data.get('email')} has no entry in the lobby, you must contact an admin to gain access to the platform"}, - status=status.HTTP_403_FORBIDDEN - ) - lobby_instance = lobby_instances[0] - # check if the verification code is valid - if lobby_instance.verification_code != data.get("verification_code"): - return Response( - {"message": "invalid verification code"}, - status=status.HTTP_403_FORBIDDEN - ) - # add the role to the request, as this was already set by an admin - if hasattr(request.data, "dict"): - request.data._mutable = True - request.data["role"] = lobby_instance.role_id - request.data._mutable = False - else: - request.data["role"] = lobby_instance.role_id - - # create a user - serializer = self.serializer_class(data=request.data) - serializer.is_valid(raise_exception=True) - - user = serializer.save(request) - # create an access and refresh token - access_token, refresh_token = jwt_encode(user) - - # add the user data to the response - response = Response(UserSerializer(user).data, status=status.HTTP_200_OK) - # set the cookie headers - set_jwt_cookies(response, access_token, refresh_token) - - return response + # validate signup + signup = SignupSerializer(data=request.data) + signup.is_valid(raise_exception=True) + # create new user + user = UserSerializer(signup.save(signup.validated_data)) + # delete the lobby entry with the user email + lobby_instance = Lobby.objects.filter(email=user.data['email']) + lobby_instance.delete() + + return post_success(user) class CustomLoginView(LoginView): From fde10f051f9907b4e988ecbeb1572a4e7b33d33b Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 22:27:19 +0200 Subject: [PATCH 0366/1000] renamed + cleaned signup serializer and view --- backend/authentication/serializers.py | 35 ++++++++------------------- backend/authentication/urls.py | 4 +-- backend/authentication/views.py | 14 +++++------ 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 2c93aa96..c3f8a424 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -17,7 +17,7 @@ from util.request_response_util import set_keys_of_instance, try_full_clean_and_save -class SignupSerializer(Serializer): +class CustomSignUpSerializer(Serializer): email = serializers.EmailField(required=True) first_name = serializers.CharField(required=True) last_name = serializers.CharField(required=True) @@ -64,11 +64,10 @@ def validate(self, data): return data - def save(self, data): - + def create(self, validated_data): user_instance = User() - set_keys_of_instance(user_instance, data, TRANSLATE) + set_keys_of_instance(user_instance, validated_data, TRANSLATE) if r := try_full_clean_and_save(user_instance): raise auth_serializers.ValidationError( @@ -79,6 +78,12 @@ def save(self, data): return user_instance + def update(self, instance, validated_data): + instance.first_name = validated_data.get("first_name", instance.first_name) + instance.last_name = validated_data.get("last_name", instance.last_name) + instance.phone_number = validated_data.get("phone_number", instance.phone_number) + return instance + class CustomTokenRefreshSerializer(Serializer): @@ -142,24 +147,4 @@ def validate_email(self, value): if not self.reset_form.is_valid(): raise serializers.ValidationError(self.reset_form.errors) - return value - - -class LobbyVerificationSerializer(Serializer): - email = serializers.EmailField() - verification_code = serializers.CharField() - - def validate_email(self, email): - lobby_instances = Lobby.objects.filter(email=email) - if not lobby_instances: - raise auth_serializers.ValidationError( - f"{email} has no entry in the lobby, you must contact an admin to gain access to the platform", - code='no_entry_in_lobby' - ) - return email - - def validate_verification_code(self, code): - lobby_instance = Lobby.objects.filter(verification_code=code).first() - if lobby_instance and lobby_instance.verification_code != code: - raise auth_serializers.ValidationError("invalid verification code", code='invalid_verification_code') - return code + return value \ No newline at end of file diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 61bfb9a4..c0625af3 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -9,12 +9,12 @@ CustomTokenVerifyView, CustomTokenRefreshView, CustomLogoutView, - CustomPasswordChangeView, SignupView, + CustomPasswordChangeView, CustomSignUpView, ) urlpatterns = [ # URLs that do not require a session or valid token - path("signup/", SignupView.as_view()), + path("signup/", CustomSignUpView.as_view()), path("password/reset/", PasswordResetView.as_view(), name="password_reset"), path("password/reset/confirm///", PasswordResetConfirmView.as_view(), name="password_reset_confirm"), path("login/", CustomLoginView.as_view()), diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 70412962..a27e1c63 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -15,29 +15,29 @@ from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView from authentication.serializers import CustomTokenRefreshSerializer, \ - CustomTokenVerifySerializer, SignupSerializer + CustomTokenVerifySerializer, CustomSignUpSerializer from base.models import Lobby from base.serializers import UserSerializer from config import settings from util.request_response_util import post_success, post_docs -class SignupView(APIView): - @extend_schema(post_docs(SignupSerializer)) +class CustomSignUpView(APIView): + @extend_schema(post_docs(CustomSignUpSerializer)) def post(self, request): """ Register a new user """ # validate signup - signup = SignupSerializer(data=request.data) + signup = CustomSignUpSerializer(data=request.data) signup.is_valid(raise_exception=True) # create new user - user = UserSerializer(signup.save(signup.validated_data)) + user = signup.save() # delete the lobby entry with the user email - lobby_instance = Lobby.objects.filter(email=user.data['email']) + lobby_instance = Lobby.objects.filter(email=user.email) lobby_instance.delete() - return post_success(user) + return post_success(UserSerializer(user)) class CustomLoginView(LoginView): From b78272734a8dd25a9acaa35bd7165f8c278d68af Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 20:49:47 +0000 Subject: [PATCH 0367/1000] Auto formatted code --- backend/authentication/serializers.py | 29 ++++++++++----------------- backend/authentication/urls.py | 3 ++- backend/authentication/views.py | 21 ++++++------------- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index c3f8a424..5178807d 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -41,24 +41,21 @@ def validate(self, data): # check if the email address is in the lobby lobby_instance = Lobby.objects.filter(email=data["email"]).first() if not lobby_instance: - raise auth_serializers.ValidationError({ - "email": - _(f"{data['email']} has no entry in the lobby, you must contact an admin to gain access to the platform"), - }) + raise auth_serializers.ValidationError( + { + "email": _( + f"{data['email']} has no entry in the lobby, you must contact an admin to gain access to the platform" + ), + } + ) # check if the verification code is valid if lobby_instance.verification_code != data["verification_code"]: - raise auth_serializers.ValidationError({ - "verification_code": - _(f"invalid verification code") - }) + raise auth_serializers.ValidationError({"verification_code": _(f"invalid verification code")}) # add role to the validated data data["role"] = lobby_instance.role_id # check if passwords match if data["password1"] != data["password2"]: - raise serializers.ValidationError({ - "message": - _("the two password fields didn't match.") - }) + raise serializers.ValidationError({"message": _("the two password fields didn't match.")}) # add password to the validated data data["password"] = data["password1"] @@ -70,9 +67,7 @@ def create(self, validated_data): set_keys_of_instance(user_instance, validated_data, TRANSLATE) if r := try_full_clean_and_save(user_instance): - raise auth_serializers.ValidationError( - r.data - ) + raise auth_serializers.ValidationError(r.data) user_instance.save() @@ -86,7 +81,6 @@ def update(self, instance, validated_data): class CustomTokenRefreshSerializer(Serializer): - def validate(self, incoming_data): # extract the request request = self.context["request"] @@ -118,7 +112,6 @@ def validate(self, incoming_data): class CustomTokenVerifySerializer(Serializer): - def validate(self, incoming_data): # extract the request request = self.context["request"] @@ -147,4 +140,4 @@ def validate_email(self, value): if not self.reset_form.is_valid(): raise serializers.ValidationError(self.reset_form.errors) - return value \ No newline at end of file + return value diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index c0625af3..8f34f6ce 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -9,7 +9,8 @@ CustomTokenVerifyView, CustomTokenRefreshView, CustomLogoutView, - CustomPasswordChangeView, CustomSignUpView, + CustomPasswordChangeView, + CustomSignUpView, ) urlpatterns = [ diff --git a/backend/authentication/views.py b/backend/authentication/views.py index a27e1c63..9873dddc 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -1,7 +1,8 @@ from dj_rest_auth.jwt_auth import ( unset_jwt_cookies, set_jwt_access_cookie, - set_jwt_refresh_cookie, set_jwt_cookies, + set_jwt_refresh_cookie, + set_jwt_cookies, ) from dj_rest_auth.views import LoginView, PasswordChangeView from django.utils.translation import gettext_lazy as _ @@ -14,8 +15,7 @@ from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView -from authentication.serializers import CustomTokenRefreshSerializer, \ - CustomTokenVerifySerializer, CustomSignUpSerializer +from authentication.serializers import CustomTokenRefreshSerializer, CustomTokenVerifySerializer, CustomSignUpSerializer from base.models import Lobby from base.serializers import UserSerializer from config import settings @@ -101,10 +101,7 @@ def post(self, request, *args, **kwargs): # get new access and refresh token data = dict(serializer.validated_data) # construct the response - response = Response( - {"message": _("refresh of tokens successful")}, - status=status.HTTP_200_OK - ) + response = Response({"message": _("refresh of tokens successful")}, status=status.HTTP_200_OK) set_jwt_access_cookie(response, data["access"]) set_jwt_refresh_cookie(response, data["refresh"]) return response @@ -122,10 +119,7 @@ def post(self, request, *args, **kwargs): except TokenError as e: raise InvalidToken(e.args[0]) - return Response( - {"message": "refresh token validation successful"}, - status=status.HTTP_200_OK - ) + return Response({"message": "refresh token validation successful"}, status=status.HTTP_200_OK) class CustomPasswordChangeView(PasswordChangeView): @@ -136,7 +130,4 @@ def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response( - {"message": _("new password has been saved")}, - status=status.HTTP_200_OK - ) + return Response({"message": _("new password has been saved")}, status=status.HTTP_200_OK) From bfed601ccc2a864977147e9f3a934ce3e070dad8 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Tue, 28 Mar 2023 23:10:18 +0200 Subject: [PATCH 0368/1000] Patch supported for name and public id of building (#189) --- frontend/lib/building.tsx | 32 +++++--- frontend/pages/syndic/building.tsx | 124 ++++++++++++++++++++++------- 2 files changed, 116 insertions(+), 40 deletions(-) diff --git a/frontend/lib/building.tsx b/frontend/lib/building.tsx index 03d979c2..3a1acb6b 100644 --- a/frontend/lib/building.tsx +++ b/frontend/lib/building.tsx @@ -2,18 +2,18 @@ import api from "@/lib/api/axios"; import { AxiosResponse } from "axios"; export interface BuildingInterface { - id: number; - city: string; - postal_code: string; - street: string; - house_number: number; - bus: number; - client_id: number; - duration: Date; - syndic_id: number; - region_id: number; - name: string; - public_id: string; + "id": number; + "syndic_id": number; + "name": string; + "city": string; + "postal_code": string; + "street": string; + "house_number": number; + "bus": string; + "region_id": number; + "duration": string; + "client_id": number; + "public_id": string; } export const getBuildingsFromOwner = async (ownerId: string): Promise> => { @@ -25,3 +25,11 @@ export const getBuildingInfo = async (buildingId: string | undefined): Promise { + const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_BUILDING}${buildingId}`; + + + console.log(`De te patchen data is ${data}`) + return await api.patch(request_url, data ); +} \ No newline at end of file diff --git a/frontend/pages/syndic/building.tsx b/frontend/pages/syndic/building.tsx index ffd9acf5..afe16453 100644 --- a/frontend/pages/syndic/building.tsx +++ b/frontend/pages/syndic/building.tsx @@ -1,5 +1,5 @@ import BaseHeader from "@/components/header/BaseHeader"; -import {BuildingInterface, getBuildingInfo} from "@/lib/building"; +import {BuildingInterface, getBuildingInfo, patchBuildingInfo} from "@/lib/building"; import {useRouter} from "next/router"; import {useEffect, useState} from "react"; import {withAuthorisation} from "@/components/withAuthorisation"; @@ -18,31 +18,86 @@ interface DashboardQuery extends ParsedUrlQuery { id?: string; } + function SyndicBuilding() { + const router = useRouter(); const query = router.query as DashboardQuery; const [building, setBuilding] = useState(null); const [editBuilding, setEditBuilding] = useState(false); + const [errorText, setErrorText] = useState(""); + + const [formData, setFormData] = useState({ + name: null, + public_id: null + }) + + const handleInputChange = (event) => { + const name = event.target.name; + const value = event.target.value; + + console.log(event.target) + console.log(`extracted name en value zijn ${name} en ${value}`) + setFormData({ + ...formData, + [name]: value, + }); + + console.log(`handleInputChange is dus gedaan, nu is formData ${JSON.stringify(formData)}`) + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + + console.log(`In handleSubmit ${JSON.stringify(formData)}`) + + let toSend = {} + for (const [key, value] of Object.entries(formData)) { + if (value) { + toSend[key] = value; + } + } + + patchBuildingInfo(query.id, formData).then(res => { + setEditBuilding(false); + setBuilding(res.data); + }).catch(error => { + console.log("We hebben een error") + setErrorText(error.response.data.detail) + console.log(error.response.data.detail) + console.log(error); + + }) + + } + + + async function fetchBuilding() { + getBuildingInfo(query.id) + .then((buildings: AxiosResponse) => { + setBuilding(buildings.data); + }) + .catch((error) => { + console.log("We hebben een error") + console.log(error); + }); + } useEffect(() => { if (!query.id) { return; } - - async function fetchBuilding() { - getBuildingInfo(query.id) - .then((buildings: AxiosResponse) => { - setBuilding(buildings.data); - }) - .catch((error) => { - console.log(error); - }); - } - fetchBuilding(); }, [query.id]); + + function get_building_key(key: string) { + if (building) + return building[key] || "/"; + return "/"; + } + return ( <> @@ -70,17 +125,30 @@ function SyndicBuilding() {
- Stad - - + + Naam + + Public id - + + + + + - De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link TODO + De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link + `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_OWNER_BUILDING}${building?.public_id}` + + {/*TODO: below line should probably a custom component with a state boolean*/} +
{errorText}
- @@ -104,16 +172,16 @@ function SyndicBuilding() { setEditBuilding(true); }}> -

Naam: {building?.name}

-

Stad: {building?.city}

-

Postcode: {building?.postal_code}

-

Straat: {building?.street}

-

Nr: {building?.house_number}

-

Bus: {building?.bus}

-

Client id: {building?.client_id}

-

Duration: {"" + building?.duration}

-

Region: {building?.region_id}

-

Public id: {building?.public_id}

+

Naam: {get_building_key("name")}

+

Stad: {get_building_key("city")}

+

Postcode: {get_building_key("postal_code")}

+

Straat: {get_building_key("street")}

+

Nr: {get_building_key("house_number")}

+

Bus: {get_building_key("bus")}

+

Region (todo): {get_building_key("region_id")}

+

Werktijd: {get_building_key("duration")}

+

Client id: {get_building_key("client_id")}

+

Public id: {get_building_key("public_id")}

https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1310&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 From adb8404efe3b95e19e642cccde24b84753f1e3e3 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Tue, 28 Mar 2023 21:13:43 +0000 Subject: [PATCH 0369/1000] Auto formatted code --- frontend/pages/syndic/dashboard.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index df6419ad..b655e0be 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -1,9 +1,8 @@ -import {withAuthorisation} from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/withAuthorisation"; import router from "next/router"; -import {BuildingInterface, getBuildingsFromOwner} from "@/lib/building"; -import {useEffect, useState} from "react"; -import {AxiosResponse} from "axios"; - +import { BuildingInterface, getBuildingsFromOwner } from "@/lib/building"; +import { useEffect, useState } from "react"; +import { AxiosResponse } from "axios"; function SyndicDashboard() { const [id, setId] = useState(""); @@ -60,7 +59,7 @@ function SyndicDashboard() { e.preventDefault(); router.push({ pathname: "building", - query: {id: building.id}, + query: { id: building.id }, }); }} > From ef23abe51e78e417119d6d76126a93e24f2bd47a Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 29 Mar 2023 00:02:31 +0200 Subject: [PATCH 0370/1000] added syndic patch permissions for building --- backend/base/permissions.py | 24 ++++++++++++++++++++++++ backend/building/views.py | 9 ++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/base/permissions.py b/backend/base/permissions.py index 33428f66..e1954c51 100644 --- a/backend/base/permissions.py +++ b/backend/base/permissions.py @@ -107,6 +107,30 @@ def has_object_permission(self, request, view, obj: Building): return False +class OwnerWithLimitedPatch(BasePermission): + """ + Checks if the syndic patches + """ + message = "You can only patch the building public id and the name of the building that you own" + + def has_permission(self, request, view): + return request.user.role.name.lower() == "syndic" + + def has_object_permission(self, request, view, obj: Building): + if request.method in SAFE_METHODS: + return True + + if request.method == "PATCH": + data = request_to_dict(request.data) + print(data.keys()) + for k in data.keys(): + if k not in ["public_id", "name"]: + return False + return True + else: + return False + + class OwnerAccount(BasePermission): """ Checks if the user owns the user account diff --git a/backend/building/views.py b/backend/building/views.py index a227c77f..1a0e2e68 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -3,12 +3,12 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.permissions import ReadOnlyOwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent +from base.permissions import ReadOnlyOwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent, IsSyndic, \ + OwnerOfBuilding, OwnerWithLimitedPatch from base.models import Building from base.serializers import BuildingSerializer from util.request_response_util import * - TRANSLATE = {"syndic": "syndic_id"} @@ -35,7 +35,7 @@ def post(self, request): class BuildingIndividualView(APIView): - permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding] + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerWithLimitedPatch] serializer_class = BuildingSerializer @extend_schema(responses=get_docs(BuildingSerializer)) @@ -112,6 +112,7 @@ def get(self, request, building_public_id): class BuildingNewPublicId(APIView): serializer_class = BuildingSerializer + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding] @extend_schema( description="Generate a new unique uuid as public id for the building.", @@ -128,6 +129,8 @@ def post(self, request, building_id): building_instance = building_instance[0] + self.check_object_permissions(request, building_instance) + building_instance.public_id = get_unique_uuid() if r := try_full_clean_and_save(building_instance): From 2583ba6a527ca8cc8f8567a344df710c86ca3372 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 22:04:56 +0000 Subject: [PATCH 0371/1000] Auto formatted code --- backend/base/permissions.py | 1 + backend/building/views.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/base/permissions.py b/backend/base/permissions.py index e1954c51..781260f7 100644 --- a/backend/base/permissions.py +++ b/backend/base/permissions.py @@ -111,6 +111,7 @@ class OwnerWithLimitedPatch(BasePermission): """ Checks if the syndic patches """ + message = "You can only patch the building public id and the name of the building that you own" def has_permission(self, request, view): diff --git a/backend/building/views.py b/backend/building/views.py index 1a0e2e68..e1e83dc6 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -3,8 +3,15 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.permissions import ReadOnlyOwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent, IsSyndic, \ - OwnerOfBuilding, OwnerWithLimitedPatch +from base.permissions import ( + ReadOnlyOwnerOfBuilding, + IsAdmin, + IsSuperStudent, + ReadOnlyStudent, + IsSyndic, + OwnerOfBuilding, + OwnerWithLimitedPatch, +) from base.models import Building from base.serializers import BuildingSerializer from util.request_response_util import * From ee6ffbf6104d85596e4225e6b954b97dfb42b519 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Wed, 29 Mar 2023 00:36:19 +0200 Subject: [PATCH 0372/1000] Added /admin/data/tours/ --- ...thorisation.tsx => with-authorisation.tsx} | 0 frontend/lib/region.tsx | 17 +++ frontend/lib/tour.tsx | 20 +++ frontend/pages/admin/dashboard.tsx | 2 +- frontend/pages/admin/data/tours/edit.tsx | 45 ++++++ frontend/pages/admin/data/tours/index.tsx | 128 +++++++++++++++++- frontend/pages/default/dashboard.tsx | 2 +- frontend/pages/student/dashboard.tsx | 2 +- frontend/pages/syndic/dashboard.tsx | 2 +- 9 files changed, 207 insertions(+), 11 deletions(-) rename frontend/components/{withAuthorisation.tsx => with-authorisation.tsx} (100%) create mode 100644 frontend/lib/region.tsx create mode 100644 frontend/lib/tour.tsx diff --git a/frontend/components/withAuthorisation.tsx b/frontend/components/with-authorisation.tsx similarity index 100% rename from frontend/components/withAuthorisation.tsx rename to frontend/components/with-authorisation.tsx diff --git a/frontend/lib/region.tsx b/frontend/lib/region.tsx new file mode 100644 index 00000000..e3e69ff1 --- /dev/null +++ b/frontend/lib/region.tsx @@ -0,0 +1,17 @@ +import { AxiosResponse } from "axios"; +import api from "@/lib/api/axios"; + +export interface Region { + id: number, + region : string +} + +export async function getAllRegions() : Promise> { + const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_REGIONS}`; + return await api.get(request_url); +} + +export async function getRegion(regionId: number) : Promise> { + const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_REGION}${regionId}`; + return await api.get(request_url); +} \ No newline at end of file diff --git a/frontend/lib/tour.tsx b/frontend/lib/tour.tsx new file mode 100644 index 00000000..70a1f9a2 --- /dev/null +++ b/frontend/lib/tour.tsx @@ -0,0 +1,20 @@ +import { AxiosResponse } from "axios"; +import api from "@/lib/api/axios"; + +export interface Tour { + id : number, + name : string, + region : number, + modified_at : Date +} + +export async function getAllTours() : Promise> { + const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_TOURS}`; + return await api.get(request_url); +} + +export async function getTour(tourId: number) : Promise> { + const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_TOUR}${tourId}`; + console.log(request_url); + return await api.get(request_url); +} \ No newline at end of file diff --git a/frontend/pages/admin/dashboard.tsx b/frontend/pages/admin/dashboard.tsx index 5a66e2f4..bf663691 100644 --- a/frontend/pages/admin/dashboard.tsx +++ b/frontend/pages/admin/dashboard.tsx @@ -7,7 +7,7 @@ import { useRouter } from "next/router"; import { getAllUsers } from "@/lib/welcome"; import Loading from "@/components/loading"; import LogoutButton from "@/components/logoutbutton"; -import { withAuthorisation } from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/with-authorisation"; function AdminDashboard() { const router = useRouter(); diff --git a/frontend/pages/admin/data/tours/edit.tsx b/frontend/pages/admin/data/tours/edit.tsx index 856dbe56..ec728c55 100644 --- a/frontend/pages/admin/data/tours/edit.tsx +++ b/frontend/pages/admin/data/tours/edit.tsx @@ -1,10 +1,55 @@ import BaseHeader from "@/components/header/BaseHeader"; +import {useEffect, useState} from "react"; +import {useRouter} from "next/router"; +import {getTour, Tour} from "@/lib/tour"; +import {getRegion, Region} from "@/lib/region"; + + +interface ParsedUrlQuery {} + +interface DataToursEditQuery extends ParsedUrlQuery { + tour?: number; +} + export default function AdminDataToursEdit() { + const router = useRouter(); + const query : DataToursEditQuery = router.query as DataToursEditQuery; + const [tour, setTour] = useState(); + const [region, setRegion] = useState(); + + useEffect(() => { + if (! query.tour) { + return; + } + + getTour(query.tour).then(res => { + console.log(res); + setTour(res.data); + }, err => { + console.error(err); + }); + }, [router.isReady]); + + useEffect(() => { + if (!tour) { + return; + } + getRegion(tour.region).then(res => { + setRegion(res.data); + }, err => { + console.error(err); + }); + }, [tour]); + return ( <> <> +

{tour? (new Date(tour.modified_at)).toLocaleString() : ""}

+

{tour?.name}

+

{region?.region}

+

https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=115-606&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486

diff --git a/frontend/pages/admin/data/tours/index.tsx b/frontend/pages/admin/data/tours/index.tsx index ef4ae364..a435df3b 100644 --- a/frontend/pages/admin/data/tours/index.tsx +++ b/frontend/pages/admin/data/tours/index.tsx @@ -1,14 +1,128 @@ import BaseHeader from "@/components/header/BaseHeader"; +import React, {useEffect, useState} from "react"; +import {getAllTours, Tour} from "@/lib/tour"; +import {Region, getAllRegions} from "@/lib/region"; +import {withAuthorisation} from "@/components/with-authorisation"; +import {useRouter} from "next/router"; + +function AdminDataTours() { + const router = useRouter(); + const [allTours, setAllTours] = useState([]); + const [tours, setTours] = useState([]); + const [regions, setRegions] = useState([]); + const defaultOption = "---"; + const [selected, setSelected] = useState(defaultOption); + const [searchInput, setSearchInput] = useState(""); + + // On refresh, get all the tours & regions + useEffect(() => { + getAllTours().then(res => { + const tours: Tour[] = res.data; + setTours(tours); + setAllTours(tours); + }, err => { + console.error(err); + }); + getAllRegions().then(res => { + let regions: Region[] = res.data; + setRegions(regions); + }, err => { + console.error(err); + }) + }, []); + + // Search in tours when the input changes + useEffect(() => { + searchTours(); + }, [searchInput]); + + // Filter the regions when the selected region changes + useEffect(() => { + filterRegions(); + }, [selected]); + + // Get the name of a region + function getRegioName(regionId: number): string { + const region: Region | undefined = regions.find((region: Region) => region.id === regionId); + if (region) { + return region.region; + } + return ""; + } + + // Filter tours based on the region + function filterRegions() { + if (selected === defaultOption) { + setTours(allTours); + return; + } + const selectedRegion: Region | undefined = regions.find((region: Region) => region.region === selected); + if (!selectedRegion) { + return; + } + const regionId: number = selectedRegion.id; + const toursInRegion = allTours.filter((tour: Tour) => tour.region === regionId); + setTours(toursInRegion); + } + + // Search based on the name of a tour + function searchTours() { + const isDefault = selected === defaultOption; + const searchTours = allTours.filter((tour: Tour) => { + const isIncluded = tour.name.toLowerCase().includes(searchInput.toLowerCase()); + const isInRegion = tour.region === regions.find((region: Region) => region.region === selected)?.id; + return isIncluded && (isDefault || isInRegion); + }); + setTours(searchTours); + } -export default function AdminDataTours() { return ( <> - <> - -

- https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=68-429&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 -

- + +
+ ) => { + setSearchInput(e.target.value); + }}> + +
+ + + + + + + + + + { + tours.map((tour: Tour, index) => { + return ( + router.push({ + pathname: "/admin/data/tours/edit", + query: {tour: tour.id} + })}> + + + + + ); + }) + } + +
NaamRegioLaatste aanpassing
{tour.name}{getRegioName(tour.region)}{(new Date(tour.modified_at)).toLocaleString()}
+

+ https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=68-429&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486 +

); } + + +export default withAuthorisation(AdminDataTours, ["Admin", "Superstudent"]); diff --git a/frontend/pages/default/dashboard.tsx b/frontend/pages/default/dashboard.tsx index 891e411b..342bb68d 100644 --- a/frontend/pages/default/dashboard.tsx +++ b/frontend/pages/default/dashboard.tsx @@ -3,7 +3,7 @@ import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; import LogoutButton from "@/components/logoutbutton"; -import { withAuthorisation } from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/with-authorisation"; function DefaultDashboard() { return ( diff --git a/frontend/pages/student/dashboard.tsx b/frontend/pages/student/dashboard.tsx index 031bd9e2..92cd4955 100644 --- a/frontend/pages/student/dashboard.tsx +++ b/frontend/pages/student/dashboard.tsx @@ -3,7 +3,7 @@ import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; import LogoutButton from "@/components/logoutbutton"; -import { withAuthorisation } from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/with-authorisation"; function StudentDashboard() { return ( diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index b97fa6c2..38b1bcb4 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -3,7 +3,7 @@ import styles from "styles/Welcome.module.css"; import soon from "public/coming_soon.png"; import Image from "next/image"; import LogoutButton from "@/components/logoutbutton"; -import { withAuthorisation } from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/with-authorisation"; function SyndicDashboard() { return ( From a2bfd468de89fa8a41c72ce03274f2d13ac60c14 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Wed, 29 Mar 2023 00:37:57 +0200 Subject: [PATCH 0373/1000] Typescript typing (#189) --- frontend/lib/building.tsx | 2 +- frontend/pages/syndic/building.tsx | 16 ++++++++-------- frontend/pages/syndic/dashboard.tsx | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/lib/building.tsx b/frontend/lib/building.tsx index 3a1acb6b..0877c010 100644 --- a/frontend/lib/building.tsx +++ b/frontend/lib/building.tsx @@ -26,7 +26,7 @@ export const getBuildingInfo = async (buildingId: string | undefined): Promise { +export const patchBuildingInfo = async (buildingId: string | undefined, data: BuildingInterface | Object) => { const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_BUILDING}${buildingId}`; diff --git a/frontend/pages/syndic/building.tsx b/frontend/pages/syndic/building.tsx index afe16453..a5acb019 100644 --- a/frontend/pages/syndic/building.tsx +++ b/frontend/pages/syndic/building.tsx @@ -1,7 +1,7 @@ import BaseHeader from "@/components/header/BaseHeader"; import {BuildingInterface, getBuildingInfo, patchBuildingInfo} from "@/lib/building"; import {useRouter} from "next/router"; -import {useEffect, useState} from "react"; +import {ChangeEvent, MouseEventHandler, useEffect, useState} from "react"; import {withAuthorisation} from "@/components/withAuthorisation"; import {AxiosResponse} from "axios"; import styles from "@/styles/Welcome.module.css"; @@ -28,12 +28,12 @@ function SyndicBuilding() { const [editBuilding, setEditBuilding] = useState(false); const [errorText, setErrorText] = useState(""); - const [formData, setFormData] = useState({ - name: null, - public_id: null + const [formData, setFormData] = useState({ + name: "", + public_id: "" }) - const handleInputChange = (event) => { + const handleInputChange = (event: ChangeEvent) => { const name = event.target.name; const value = event.target.value; @@ -47,12 +47,12 @@ function SyndicBuilding() { console.log(`handleInputChange is dus gedaan, nu is formData ${JSON.stringify(formData)}`) }; - const handleSubmit = async (event) => { - event.preventDefault(); + const handleSubmit = async (event: MouseEventHandler) => { + //event.preventDefault(); console.log(`In handleSubmit ${JSON.stringify(formData)}`) - let toSend = {} + let toSend: Object = {} for (const [key, value] of Object.entries(formData)) { if (value) { toSend[key] = value; diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index b655e0be..04ddfc0b 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -1,8 +1,8 @@ -import { withAuthorisation } from "@/components/withAuthorisation"; +import {withAuthorisation} from "@/components/withAuthorisation"; import router from "next/router"; -import { BuildingInterface, getBuildingsFromOwner } from "@/lib/building"; -import { useEffect, useState } from "react"; -import { AxiosResponse } from "axios"; +import {BuildingInterface, getBuildingsFromOwner} from "@/lib/building"; +import {useEffect, useState} from "react"; +import {AxiosResponse} from "axios"; function SyndicDashboard() { const [id, setId] = useState(""); @@ -59,7 +59,7 @@ function SyndicDashboard() { e.preventDefault(); router.push({ pathname: "building", - query: { id: building.id }, + query: {id: building.id}, }); }} > From 890716689e3f31f6e9e0405db659b1efa4b3534a Mon Sep 17 00:00:00 2001 From: TiboStr Date: Tue, 28 Mar 2023 22:38:53 +0000 Subject: [PATCH 0374/1000] Auto formatted code --- frontend/lib/building.tsx | 31 +++---- frontend/pages/syndic/building.tsx | 136 +++++++++++++++------------- frontend/pages/syndic/dashboard.tsx | 10 +- 3 files changed, 91 insertions(+), 86 deletions(-) diff --git a/frontend/lib/building.tsx b/frontend/lib/building.tsx index 0877c010..fea2f1c1 100644 --- a/frontend/lib/building.tsx +++ b/frontend/lib/building.tsx @@ -2,18 +2,18 @@ import api from "@/lib/api/axios"; import { AxiosResponse } from "axios"; export interface BuildingInterface { - "id": number; - "syndic_id": number; - "name": string; - "city": string; - "postal_code": string; - "street": string; - "house_number": number; - "bus": string; - "region_id": number; - "duration": string; - "client_id": number; - "public_id": string; + id: number; + syndic_id: number; + name: string; + city: string; + postal_code: string; + street: string; + house_number: number; + bus: string; + region_id: number; + duration: string; + client_id: number; + public_id: string; } export const getBuildingsFromOwner = async (ownerId: string): Promise> => { @@ -29,7 +29,6 @@ export const getBuildingInfo = async (buildingId: string | undefined): Promise { const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_BUILDING}${buildingId}`; - - console.log(`De te patchen data is ${data}`) - return await api.patch(request_url, data ); -} \ No newline at end of file + console.log(`De te patchen data is ${data}`); + return await api.patch(request_url, data); +}; diff --git a/frontend/pages/syndic/building.tsx b/frontend/pages/syndic/building.tsx index a5acb019..e818a63a 100644 --- a/frontend/pages/syndic/building.tsx +++ b/frontend/pages/syndic/building.tsx @@ -1,26 +1,23 @@ import BaseHeader from "@/components/header/BaseHeader"; -import {BuildingInterface, getBuildingInfo, patchBuildingInfo} from "@/lib/building"; -import {useRouter} from "next/router"; -import {ChangeEvent, MouseEventHandler, useEffect, useState} from "react"; -import {withAuthorisation} from "@/components/withAuthorisation"; -import {AxiosResponse} from "axios"; +import { BuildingInterface, getBuildingInfo, patchBuildingInfo } from "@/lib/building"; +import { useRouter } from "next/router"; +import { ChangeEvent, MouseEventHandler, useEffect, useState } from "react"; +import { withAuthorisation } from "@/components/withAuthorisation"; +import { AxiosResponse } from "axios"; import styles from "@/styles/Welcome.module.css"; -import {Button, Form, Modal} from "react-bootstrap"; -import {TiPencil} from "react-icons/ti"; +import { Button, Form, Modal } from "react-bootstrap"; +import { TiPencil } from "react-icons/ti"; import Image from "next/image"; import soon from "@/public/coming_soon.png"; import LogoutButton from "@/components/logoutbutton"; -interface ParsedUrlQuery { -} +interface ParsedUrlQuery {} interface DashboardQuery extends ParsedUrlQuery { id?: string; } - function SyndicBuilding() { - const router = useRouter(); const query = router.query as DashboardQuery; @@ -30,48 +27,47 @@ function SyndicBuilding() { const [formData, setFormData] = useState({ name: "", - public_id: "" - }) + public_id: "", + }); const handleInputChange = (event: ChangeEvent) => { const name = event.target.name; const value = event.target.value; - console.log(event.target) - console.log(`extracted name en value zijn ${name} en ${value}`) + console.log(event.target); + console.log(`extracted name en value zijn ${name} en ${value}`); setFormData({ ...formData, [name]: value, }); - console.log(`handleInputChange is dus gedaan, nu is formData ${JSON.stringify(formData)}`) + console.log(`handleInputChange is dus gedaan, nu is formData ${JSON.stringify(formData)}`); }; const handleSubmit = async (event: MouseEventHandler) => { //event.preventDefault(); - console.log(`In handleSubmit ${JSON.stringify(formData)}`) + console.log(`In handleSubmit ${JSON.stringify(formData)}`); - let toSend: Object = {} + let toSend: Object = {}; for (const [key, value] of Object.entries(formData)) { if (value) { toSend[key] = value; } } - patchBuildingInfo(query.id, formData).then(res => { - setEditBuilding(false); - setBuilding(res.data); - }).catch(error => { - console.log("We hebben een error") - setErrorText(error.response.data.detail) - console.log(error.response.data.detail) - console.log(error); - - }) - - } - + patchBuildingInfo(query.id, formData) + .then((res) => { + setEditBuilding(false); + setBuilding(res.data); + }) + .catch((error) => { + console.log("We hebben een error"); + setErrorText(error.response.data.detail); + console.log(error.response.data.detail); + console.log(error); + }); + }; async function fetchBuilding() { getBuildingInfo(query.id) @@ -79,7 +75,7 @@ function SyndicBuilding() { setBuilding(buildings.data); }) .catch((error) => { - console.log("We hebben een error") + console.log("We hebben een error"); console.log(error); }); } @@ -91,16 +87,14 @@ function SyndicBuilding() { fetchBuilding(); }, [query.id]); - function get_building_key(key: string) { - if (building) - return building[key] || "/"; + if (building) return building[key] || "/"; return "/"; } return ( <> - +
Welcome to the Syndic Dashboard! setEditBuilding(false)}> - - Bewerk gebouw - + Bewerk gebouw Naam - + Public id - - - + + - De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link - `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_OWNER_BUILDING}${building?.public_id}` - + De inwoners van uw gebouw kunnen info over vuilnisophaling zien op de link{" "} + + `${process.env.NEXT_PUBLIC_BASE_API_URL}$ + {process.env.NEXT_PUBLIC_API_OWNER_BUILDING}${building?.public_id}` + {/*TODO: below line should probably a custom component with a state boolean*/} -
{errorText}
- - - -

- Gebouw { - e.preventDefault(); - setEditBuilding(true); - }}> + Gebouw{" "} + { + e.preventDefault(); + setEditBuilding(true); + }} + >

Naam: {get_building_key("name")}

Stad: {get_building_key("city")}

@@ -186,8 +192,8 @@ function SyndicBuilding() {

https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=16-1310&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486

- Site coming soon - + Site coming soon + ); } diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index 04ddfc0b..b655e0be 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -1,8 +1,8 @@ -import {withAuthorisation} from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/withAuthorisation"; import router from "next/router"; -import {BuildingInterface, getBuildingsFromOwner} from "@/lib/building"; -import {useEffect, useState} from "react"; -import {AxiosResponse} from "axios"; +import { BuildingInterface, getBuildingsFromOwner } from "@/lib/building"; +import { useEffect, useState } from "react"; +import { AxiosResponse } from "axios"; function SyndicDashboard() { const [id, setId] = useState(""); @@ -59,7 +59,7 @@ function SyndicDashboard() { e.preventDefault(); router.push({ pathname: "building", - query: {id: building.id}, + query: { id: building.id }, }); }} > From b3ed5ca53ee17a58fd74ab5d82d0bf2bea45b08f Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 29 Mar 2023 00:39:14 +0200 Subject: [PATCH 0375/1000] everyone can see all building information when they have a public link to it --- backend/building/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/building/views.py b/backend/building/views.py index 353ce5d1..ee6e0c4f 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -103,8 +103,6 @@ def get(self, request, building_public_id): building_instance = building_instance[0] - # TODO: should the general public see all data about a building? - # Discuss this when writing tests for building return get_success(BuildingSerializer(building_instance)) From a78fd2c66fd78ff5cf5608065cf01033778a4267 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Wed, 29 Mar 2023 00:55:06 +0200 Subject: [PATCH 0376/1000] new custom logout serializer --- backend/authentication/serializers.py | 47 ++++++++++++++++++++++- backend/authentication/views.py | 55 +++++++++------------------ 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 5178807d..e154aec7 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,17 +1,21 @@ from allauth.account.adapter import get_adapter from allauth.utils import email_address_exists from dj_rest_auth import serializers as auth_serializers -from dj_rest_auth.serializers import PasswordResetSerializer +from dj_rest_auth.jwt_auth import unset_jwt_cookies +from dj_rest_auth.serializers import PasswordResetSerializer, LoginSerializer from django.utils.translation import gettext_lazy as _ from phonenumber_field.serializerfields import PhoneNumberField -from rest_framework import serializers +from rest_framework import serializers, status from rest_framework.exceptions import ValidationError +from rest_framework.response import Response from rest_framework.serializers import Serializer +from rest_framework_simplejwt.exceptions import TokenError from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken from rest_framework_simplejwt.tokens import RefreshToken from authentication.forms import CustomAllAuthPasswordResetForm from base.models import User, Lobby +from base.serializers import UserSerializer from config import settings from users.views import TRANSLATE from util.request_response_util import set_keys_of_instance, try_full_clean_and_save @@ -141,3 +145,42 @@ def validate_email(self, value): raise serializers.ValidationError(self.reset_form.errors) return value + + +class CustomLoginResponseSerializer(LoginSerializer): + message = serializers.CharField() + user = UserSerializer() + + +class CustomLogoutSerializer(serializers.Serializer): + message = serializers.CharField() + + def logout_user(self, request): + response = Response( + {"message": _("successfully logged out")}, + status=status.HTTP_200_OK, + ) + + cookie_name = getattr(settings, "JWT_AUTH_REFRESH_COOKIE", None) + try: + if cookie_name and cookie_name in request.COOKIES: + token = RefreshToken(request.COOKIES.get(cookie_name)) + token.blacklist() + except KeyError: + response.data = {"message": _("refresh token was not included in request cookies")} + response.status_code = status.HTTP_401_UNAUTHORIZED + except (TokenError, AttributeError, TypeError) as error: + if hasattr(error, "args"): + if "Token is blacklisted" in error.args or "Token is invalid or expired" in error.args: + response.data = {"message": _(error.args[0].lower())} + response.status_code = status.HTTP_401_UNAUTHORIZED + else: + response.data = {"message": _("an error has occurred.")} + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + else: + response.data = {"message": _("an error has occurred.")} + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + + unset_jwt_cookies(response) + + return response diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 9873dddc..a64d9eda 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -1,5 +1,4 @@ from dj_rest_auth.jwt_auth import ( - unset_jwt_cookies, set_jwt_access_cookie, set_jwt_refresh_cookie, set_jwt_cookies, @@ -12,18 +11,20 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.exceptions import TokenError, InvalidToken -from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView -from authentication.serializers import CustomTokenRefreshSerializer, CustomTokenVerifySerializer, CustomSignUpSerializer +from authentication.serializers import CustomTokenRefreshSerializer, CustomTokenVerifySerializer, \ + CustomSignUpSerializer, CustomLogoutSerializer, CustomLoginResponseSerializer from base.models import Lobby from base.serializers import UserSerializer -from config import settings -from util.request_response_util import post_success, post_docs +from util.request_response_util import post_success class CustomSignUpView(APIView): - @extend_schema(post_docs(CustomSignUpSerializer)) + @extend_schema( + request={None: CustomSignUpSerializer}, + responses={200: UserSerializer} + ) def post(self, request): """ Register a new user @@ -41,12 +42,17 @@ def post(self, request): class CustomLoginView(LoginView): + + @extend_schema(responses={200: CustomLoginResponseSerializer}) + def post(self, request, *args, **kwargs): + return super().post(request, *args, **kwargs) + def get_response(self): - data = { + serializer = CustomLoginResponseSerializer(data={ "message": _("successful login"), "user": UserSerializer(self.user).data, - } - response = Response(data, status=status.HTTP_200_OK) + }) + response = Response(serializer.data, status=status.HTTP_200_OK) set_jwt_cookies(response, self.access_token, self.refresh_token) return response @@ -54,35 +60,10 @@ def get_response(self): class CustomLogoutView(APIView): permission_classes = [IsAuthenticated] - @extend_schema(responses={200: None, 401: None, 500: None}) + @extend_schema(responses={200: CustomLogoutSerializer, 401: CustomLogoutSerializer, 500: CustomLogoutSerializer}) def post(self, request): - response = Response( - {"message": _("successfully logged out")}, - status=status.HTTP_200_OK, - ) - - cookie_name = getattr(settings, "JWT_AUTH_REFRESH_COOKIE", None) - try: - if cookie_name and cookie_name in request.COOKIES: - token = RefreshToken(request.COOKIES.get(cookie_name)) - token.blacklist() - except KeyError: - response.data = {"message": _("refresh token was not included in request cookies")} - response.status_code = status.HTTP_401_UNAUTHORIZED - except (TokenError, AttributeError, TypeError) as error: - if hasattr(error, "args"): - if "Token is blacklisted" in error.args or "Token is invalid or expired" in error.args: - response.data = {"message": _(error.args[0].lower())} - response.status_code = status.HTTP_401_UNAUTHORIZED - else: - response.data = {"message": _("an error has occurred.")} - response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - else: - response.data = {"message": _("an error has occurred.")} - response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - - unset_jwt_cookies(response) - + serializer = CustomLogoutSerializer() + response = serializer.logout_user(request) return response From 7cf36dee0dc5b43b63026dbcfc06e5e63e999404 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Tue, 28 Mar 2023 22:55:47 +0000 Subject: [PATCH 0377/1000] Auto formatted code --- backend/authentication/views.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/authentication/views.py b/backend/authentication/views.py index a64d9eda..ea01238b 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -13,18 +13,20 @@ from rest_framework_simplejwt.exceptions import TokenError, InvalidToken from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView -from authentication.serializers import CustomTokenRefreshSerializer, CustomTokenVerifySerializer, \ - CustomSignUpSerializer, CustomLogoutSerializer, CustomLoginResponseSerializer +from authentication.serializers import ( + CustomTokenRefreshSerializer, + CustomTokenVerifySerializer, + CustomSignUpSerializer, + CustomLogoutSerializer, + CustomLoginResponseSerializer, +) from base.models import Lobby from base.serializers import UserSerializer from util.request_response_util import post_success class CustomSignUpView(APIView): - @extend_schema( - request={None: CustomSignUpSerializer}, - responses={200: UserSerializer} - ) + @extend_schema(request={None: CustomSignUpSerializer}, responses={200: UserSerializer}) def post(self, request): """ Register a new user @@ -42,16 +44,17 @@ def post(self, request): class CustomLoginView(LoginView): - @extend_schema(responses={200: CustomLoginResponseSerializer}) def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs) def get_response(self): - serializer = CustomLoginResponseSerializer(data={ - "message": _("successful login"), - "user": UserSerializer(self.user).data, - }) + serializer = CustomLoginResponseSerializer( + data={ + "message": _("successful login"), + "user": UserSerializer(self.user).data, + } + ) response = Response(serializer.data, status=status.HTTP_200_OK) set_jwt_cookies(response, self.access_token, self.refresh_token) return response From 7a48226959742e12fd10ff7f3b1ad9165f0e9abc Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Wed, 29 Mar 2023 09:37:46 +0200 Subject: [PATCH 0378/1000] Started specific tour page --- frontend/lib/building-on-tour.tsx | 7 +++++++ frontend/pages/admin/data/tours/edit.tsx | 1 + 2 files changed, 8 insertions(+) create mode 100644 frontend/lib/building-on-tour.tsx diff --git a/frontend/lib/building-on-tour.tsx b/frontend/lib/building-on-tour.tsx new file mode 100644 index 00000000..4b93bcc0 --- /dev/null +++ b/frontend/lib/building-on-tour.tsx @@ -0,0 +1,7 @@ +import { AxiosResponse } from "axios"; +import api from "@/lib/api/axios"; + +export async function getAllBuildingsOnTour() : Promise> { + const request_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_BUILDINGS_ON_TOUR}`; + return api.get(request_url); +} \ No newline at end of file diff --git a/frontend/pages/admin/data/tours/edit.tsx b/frontend/pages/admin/data/tours/edit.tsx index ec728c55..2762a1e7 100644 --- a/frontend/pages/admin/data/tours/edit.tsx +++ b/frontend/pages/admin/data/tours/edit.tsx @@ -17,6 +17,7 @@ export default function AdminDataToursEdit() { const query : DataToursEditQuery = router.query as DataToursEditQuery; const [tour, setTour] = useState(); const [region, setRegion] = useState(); + const [allBuildings, setAllBuildings] = useState() useEffect(() => { if (! query.tour) { From 9b96d181ec48482458d61f30a4efe9df07d35cf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 11:10:12 +0000 Subject: [PATCH 0379/1000] Bump @types/node from 18.14.2 to 18.15.11 in /frontend Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.14.2 to 18.15.11. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 14 +++++++------- frontend/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d68354cd..109f2823 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "my-app", "version": "0.1.0", "dependencies": { - "@types/node": "18.14.2", + "@types/node": "18.15.11", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "axios": "^1.3.4", @@ -272,9 +272,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz", - "integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -849,9 +849,9 @@ "dev": true }, "@types/node": { - "version": "18.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz", - "integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==" + "version": "18.15.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "@types/prop-types": { "version": "15.7.5", diff --git a/frontend/package.json b/frontend/package.json index afba857d..a33ca1ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@types/node": "18.14.2", + "@types/node": "18.15.11", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "axios": "^1.3.4", From aba07e118f491cae8e7adcef8a7e232161a42059 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Wed, 29 Mar 2023 13:32:01 +0200 Subject: [PATCH 0380/1000] There is a URL to retrieve all buildings on a tour (fixes #201) --- backend/tour/urls.py | 3 ++- backend/tour/views.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/tour/urls.py b/backend/tour/urls.py index 2fc8c8f4..7194a27b 100644 --- a/backend/tour/urls.py +++ b/backend/tour/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import TourIndividualView, AllToursView, Default +from .views import TourIndividualView, AllToursView, Default, AllBuildingsOnTourView urlpatterns = [ path("/", TourIndividualView.as_view()), + path("/buildings/", AllBuildingsOnTourView.as_view()), path("all/", AllToursView.as_view()), path("", Default.as_view()), ] diff --git a/backend/tour/views.py b/backend/tour/views.py index c453096b..8138d06e 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -2,8 +2,8 @@ from rest_framework.views import APIView from base.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent -from base.models import Tour -from base.serializers import TourSerializer +from base.models import Tour, BuildingOnTour, Building +from base.serializers import TourSerializer, BuildingSerializer from util.request_response_util import * from drf_spectacular.utils import extend_schema @@ -82,10 +82,28 @@ def delete(self, request, tour_id): return delete_success() +class AllBuildingsOnTourView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] + serializer_class = TourSerializer + + @extend_schema(responses=get_docs(TourSerializer)) + def get(self, request, tour_id): + """ + Get all buildings on a tour with given id + """ + building_on_tour_instances = BuildingOnTour.objects.filter(tour_id=tour_id) + building_instances = Building.objects.filter( + id__in=building_on_tour_instances.values_list("building_id", flat=True)) + + serializer = BuildingSerializer(building_instances, many=True) + return get_success(serializer) + + class AllToursView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = TourSerializer + @extend_schema(responses=get_docs(TourSerializer)) def get(self, request): """ Get all tours From d23742ed8f60476af70bcb1b137f52ad527b3b97 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Wed, 29 Mar 2023 11:33:45 +0000 Subject: [PATCH 0381/1000] Auto formatted code --- backend/tour/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/tour/views.py b/backend/tour/views.py index 8138d06e..9c7ba6dc 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -93,7 +93,8 @@ def get(self, request, tour_id): """ building_on_tour_instances = BuildingOnTour.objects.filter(tour_id=tour_id) building_instances = Building.objects.filter( - id__in=building_on_tour_instances.values_list("building_id", flat=True)) + id__in=building_on_tour_instances.values_list("building_id", flat=True) + ) serializer = BuildingSerializer(building_instances, many=True) return get_success(serializer) From 0866d756458688fd7e8789b6e5ea664487d17457 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Wed, 29 Mar 2023 14:19:53 +0200 Subject: [PATCH 0382/1000] URL to retrieve all BuildingOnTours with given tour id (#201) --- backend/building_on_tour/urls.py | 3 ++- backend/building_on_tour/views.py | 18 ++++++++++++++++-- backend/tour/views.py | 6 +++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/building_on_tour/urls.py b/backend/building_on_tour/urls.py index 8bb25d7c..c93d10dd 100644 --- a/backend/building_on_tour/urls.py +++ b/backend/building_on_tour/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import BuildingTourIndividualView, AllBuildingToursView, Default +from .views import BuildingTourIndividualView, AllBuildingToursView, Default, AllBuilingsOnTourInTourView urlpatterns = [ path("/", BuildingTourIndividualView.as_view()), + path("tour//", AllBuilingsOnTourInTourView.as_view()), path("all/", AllBuildingToursView.as_view()), path("", Default.as_view()), ] diff --git a/backend/building_on_tour/views.py b/backend/building_on_tour/views.py index b50fa107..8603b755 100644 --- a/backend/building_on_tour/views.py +++ b/backend/building_on_tour/views.py @@ -1,9 +1,9 @@ +from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from drf_spectacular.utils import extend_schema -from base.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent from base.models import BuildingOnTour +from base.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent from base.serializers import BuildingTourSerializer from util.request_response_util import * @@ -83,6 +83,20 @@ def delete(self, request, building_tour_id): return delete_success() +class AllBuilingsOnTourInTourView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] + serializer_class = BuildingTourSerializer + + @extend_schema(responses=get_docs(BuildingTourSerializer)) + def get(self, request, tour_id): + """ + Get all BuildingsOnTour with given tour id + """ + building_on_tour_instances = BuildingOnTour.objects.filter(tour_id=tour_id) + serializer = BuildingTourSerializer(building_on_tour_instances, many=True) + return get_success(serializer) + + class AllBuildingToursView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = BuildingTourSerializer diff --git a/backend/tour/views.py b/backend/tour/views.py index 8138d06e..6ab13c6e 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -1,11 +1,11 @@ +from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent from base.models import Tour, BuildingOnTour, Building -from base.serializers import TourSerializer, BuildingSerializer +from base.permissions import IsAdmin, IsSuperStudent, ReadOnlyStudent +from base.serializers import TourSerializer, BuildingTourSerializer, BuildingSerializer from util.request_response_util import * -from drf_spectacular.utils import extend_schema TRANSLATE = {"region": "region_id"} From 2c9d34a60d46c46a3c04ac8c2199a11a2699f6d9 Mon Sep 17 00:00:00 2001 From: n00bS-oWn-m3 Date: Wed, 29 Mar 2023 17:16:19 +0200 Subject: [PATCH 0383/1000] #208 dependabot ignore major changes for frontend (no sufficient testing) --- .github/dependabot.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e65de6d5..98927951 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,6 +13,9 @@ updates: - "n00bS-oWn-m3" open-pull-requests-limit: 10 target-branch: "develop" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] - package-ecosystem: "pip" directory: "/backend" From 623d7f0e899c7315ee9d1f925287d263c8a65a6b Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Wed, 29 Mar 2023 18:01:24 +0200 Subject: [PATCH 0384/1000] Added material table to /data/tours --- frontend/package-lock.json | 242 +++++----------------- frontend/package.json | 4 + frontend/pages/admin/data/tours/index.tsx | 144 ++++++------- frontend/pages/syndic/building.tsx | 2 +- frontend/pages/syndic/dashboard.tsx | 2 +- 5 files changed, 129 insertions(+), 265 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e694bdc5..4f4c3914 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,10 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@mui/icons-material": "^5.11.11", + "@mui/material": "^5.11.15", "@types/node": "18.14.2", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", @@ -35,7 +39,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "peer": true, "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -47,7 +50,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "peer": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -59,7 +61,6 @@ "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -68,7 +69,6 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -77,7 +77,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -102,7 +101,6 @@ "version": "7.21.3", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", - "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -116,7 +114,6 @@ "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", - "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -135,7 +132,6 @@ "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", - "peer": true, "dependencies": { "@emotion/memoize": "^0.8.0", "@emotion/sheet": "^1.2.1", @@ -147,14 +143,12 @@ "node_modules/@emotion/hash": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", - "peer": true + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", - "peer": true, "dependencies": { "@emotion/memoize": "^0.8.0" } @@ -162,14 +156,12 @@ "node_modules/@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", - "peer": true + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, "node_modules/@emotion/react": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.6", @@ -193,7 +185,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", - "peer": true, "dependencies": { "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", @@ -205,14 +196,12 @@ "node_modules/@emotion/sheet": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==", - "peer": true + "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" }, "node_modules/@emotion/styled": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz", "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.6", @@ -234,14 +223,12 @@ "node_modules/@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", - "peer": true + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", - "peer": true, "peerDependencies": { "react": ">=16.8.0" } @@ -249,20 +236,17 @@ "node_modules/@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", - "peer": true + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==", - "peer": true + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, "node_modules/@mui/base": { "version": "5.0.0-alpha.123", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.123.tgz", "integrity": "sha512-pxzcAfET3I6jvWqS4kijiLMn1OmdMw+mGmDa0SqmDZo3bXXdvLhpCCPqCkULG3UykhvFCOcU5HclOX3JCA+Zhg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0", "@emotion/is-prop-valid": "^1.2.0", @@ -294,14 +278,12 @@ "node_modules/@mui/base/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==", - "peer": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@mui/core-downloads-tracker": { "version": "5.11.15", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.15.tgz", "integrity": "sha512-Q0e2oBsjHyIWWj1wLzl14btunvBYC0yl+px7zL9R69tF87uenj6q72ieS369BJ6jxYpJwvXfR6/f+TC+ZUsKKg==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui" @@ -311,7 +293,6 @@ "version": "5.11.11", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.11.tgz", "integrity": "sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -337,7 +318,6 @@ "version": "5.11.15", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.15.tgz", "integrity": "sha512-E5RbLq9/OvRKmGyeZawdnmFBCvhKkI/Zqgr0xFqW27TGwKLxObq/BreJc6Uu5Sbv8Fjj34vEAbRx6otfOyxn5w==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0", "@mui/base": "5.0.0-alpha.123", @@ -381,14 +361,12 @@ "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==", - "peer": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@mui/private-theming": { "version": "5.11.13", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.13.tgz", "integrity": "sha512-PJnYNKzW5LIx3R+Zsp6WZVPs6w5sEKJ7mgLNnUXuYB1zo5aX71FVLtV7geyPXRcaN2tsoRNK7h444ED0t7cIjA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0", "@mui/utils": "^5.11.13", @@ -415,7 +393,6 @@ "version": "5.11.11", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.11.tgz", "integrity": "sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0", "@emotion/cache": "^11.10.5", @@ -447,7 +424,6 @@ "version": "5.11.15", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.15.tgz", "integrity": "sha512-vCatoWCTnAPquoNifHbqMCMnOElEbLosVUeW0FQDyjCq+8yMABD9E6iY0s14O7iq1wD+qqU7rFAuDIVvJ/AzzA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0", "@mui/private-theming": "^5.11.13", @@ -487,7 +463,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", - "peer": true, "peerDependencies": { "@types/react": "*" }, @@ -501,7 +476,6 @@ "version": "5.11.13", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.13.tgz", "integrity": "sha512-5ltA58MM9euOuUcnvwFJqpLdEugc9XFsRR8Gt4zZNb31XzMfSKJPR4eumulyhsOTK1rWf7K4D63NKFPfX0AxqA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0", "@types/prop-types": "^15.7.5", @@ -523,8 +497,7 @@ "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==", - "peer": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@next/env": { "version": "13.2.1", @@ -889,8 +862,7 @@ "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "peer": true + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -919,7 +891,6 @@ "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "peer": true, "dependencies": { "@types/react": "*" } @@ -946,7 +917,6 @@ "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==", - "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -973,7 +943,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -1006,7 +975,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "peer": true, "engines": { "node": ">=6" } @@ -1030,7 +998,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1044,7 +1011,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "peer": true, "engines": { "node": ">=0.8.0" } @@ -1063,7 +1029,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "peer": true, "engines": { "node": ">=6" } @@ -1072,7 +1037,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -1080,8 +1044,7 @@ "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==", - "peer": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1097,14 +1060,12 @@ "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==", - "peer": true + "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==", - "peer": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -1158,7 +1119,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "peer": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -1167,7 +1127,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "peer": true, "engines": { "node": ">=10" }, @@ -1178,8 +1137,7 @@ "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==", - "peer": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/follow-redirects": { "version": "1.15.2", @@ -1216,14 +1174,12 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "peer": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "peer": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -1235,7 +1191,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "peer": true, "engines": { "node": ">=4" } @@ -1253,7 +1208,6 @@ "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==", - "peer": true, "dependencies": { "react-is": "^16.7.0" } @@ -1292,7 +1246,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1315,14 +1268,12 @@ "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==", - "peer": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "peer": true, "dependencies": { "has": "^1.0.3" }, @@ -1346,14 +1297,12 @@ "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==", - "peer": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "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==", - "peer": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -1489,7 +1438,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==", - "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -1501,7 +1449,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -1518,14 +1465,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==", - "peer": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "peer": true, "engines": { "node": ">=8" } @@ -1705,7 +1650,6 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "peer": true, "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -1722,7 +1666,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "peer": true, "engines": { "node": ">=4" } @@ -1739,7 +1682,6 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1777,14 +1719,12 @@ "node_modules/stylis": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==", - "peer": true + "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" }, "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==", - "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -1796,7 +1736,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==", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -1822,7 +1761,6 @@ "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==", - "peer": true, "engines": { "node": ">=4" } @@ -1886,7 +1824,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "peer": true, "engines": { "node": ">= 6" } @@ -1897,7 +1834,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "peer": true, "requires": { "@babel/highlight": "^7.18.6" } @@ -1906,7 +1842,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "peer": true, "requires": { "@babel/types": "^7.18.6" } @@ -1914,20 +1849,17 @@ "@babel/helper-string-parser": { "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "peer": true + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" }, "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "peer": true + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" }, "@babel/highlight": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "peer": true, "requires": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -1946,7 +1878,6 @@ "version": "7.21.3", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", - "peer": true, "requires": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -1957,7 +1888,6 @@ "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", - "peer": true, "requires": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -1976,7 +1906,6 @@ "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", - "peer": true, "requires": { "@emotion/memoize": "^0.8.0", "@emotion/sheet": "^1.2.1", @@ -1988,14 +1917,12 @@ "@emotion/hash": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==", - "peer": true + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "@emotion/is-prop-valid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", - "peer": true, "requires": { "@emotion/memoize": "^0.8.0" } @@ -2003,14 +1930,12 @@ "@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==", - "peer": true + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, "@emotion/react": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", - "peer": true, "requires": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.6", @@ -2026,7 +1951,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", - "peer": true, "requires": { "@emotion/hash": "^0.9.0", "@emotion/memoize": "^0.8.0", @@ -2038,14 +1962,12 @@ "@emotion/sheet": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==", - "peer": true + "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" }, "@emotion/styled": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz", "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==", - "peer": true, "requires": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.10.6", @@ -2058,33 +1980,28 @@ "@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==", - "peer": true + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", - "peer": true, "requires": {} }, "@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==", - "peer": true + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" }, "@emotion/weak-memoize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==", - "peer": true + "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, "@mui/base": { "version": "5.0.0-alpha.123", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.123.tgz", "integrity": "sha512-pxzcAfET3I6jvWqS4kijiLMn1OmdMw+mGmDa0SqmDZo3bXXdvLhpCCPqCkULG3UykhvFCOcU5HclOX3JCA+Zhg==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0", "@emotion/is-prop-valid": "^1.2.0", @@ -2099,22 +2016,19 @@ "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" } } }, "@mui/core-downloads-tracker": { "version": "5.11.15", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.15.tgz", - "integrity": "sha512-Q0e2oBsjHyIWWj1wLzl14btunvBYC0yl+px7zL9R69tF87uenj6q72ieS369BJ6jxYpJwvXfR6/f+TC+ZUsKKg==", - "peer": true + "integrity": "sha512-Q0e2oBsjHyIWWj1wLzl14btunvBYC0yl+px7zL9R69tF87uenj6q72ieS369BJ6jxYpJwvXfR6/f+TC+ZUsKKg==" }, "@mui/icons-material": { "version": "5.11.11", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.11.tgz", "integrity": "sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0" } @@ -2123,7 +2037,6 @@ "version": "5.11.15", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.15.tgz", "integrity": "sha512-E5RbLq9/OvRKmGyeZawdnmFBCvhKkI/Zqgr0xFqW27TGwKLxObq/BreJc6Uu5Sbv8Fjj34vEAbRx6otfOyxn5w==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0", "@mui/base": "5.0.0-alpha.123", @@ -2142,8 +2055,7 @@ "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" } } }, @@ -2151,7 +2063,6 @@ "version": "5.11.13", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.13.tgz", "integrity": "sha512-PJnYNKzW5LIx3R+Zsp6WZVPs6w5sEKJ7mgLNnUXuYB1zo5aX71FVLtV7geyPXRcaN2tsoRNK7h444ED0t7cIjA==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0", "@mui/utils": "^5.11.13", @@ -2162,7 +2073,6 @@ "version": "5.11.11", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.11.tgz", "integrity": "sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0", "@emotion/cache": "^11.10.5", @@ -2174,7 +2084,6 @@ "version": "5.11.15", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.15.tgz", "integrity": "sha512-vCatoWCTnAPquoNifHbqMCMnOElEbLosVUeW0FQDyjCq+8yMABD9E6iY0s14O7iq1wD+qqU7rFAuDIVvJ/AzzA==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0", "@mui/private-theming": "^5.11.13", @@ -2190,14 +2099,12 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", - "peer": true, "requires": {} }, "@mui/utils": { "version": "5.11.13", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.13.tgz", "integrity": "sha512-5ltA58MM9euOuUcnvwFJqpLdEugc9XFsRR8Gt4zZNb31XzMfSKJPR4eumulyhsOTK1rWf7K4D63NKFPfX0AxqA==", - "peer": true, "requires": { "@babel/runtime": "^7.21.0", "@types/prop-types": "^15.7.5", @@ -2209,8 +2116,7 @@ "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" } } }, @@ -2409,8 +2315,7 @@ "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "peer": true + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, "@types/prop-types": { "version": "15.7.5", @@ -2439,7 +2344,6 @@ "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "peer": true, "requires": { "@types/react": "*" } @@ -2466,7 +2370,6 @@ "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==", - "peer": true, "requires": { "color-convert": "^1.9.0" } @@ -2490,7 +2393,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "peer": true, "requires": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -2506,8 +2408,7 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "peer": true + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "caniuse-lite": { "version": "1.0.30001458", @@ -2518,7 +2419,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "peer": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2528,8 +2428,7 @@ "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==", - "peer": true + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" } } }, @@ -2546,14 +2445,12 @@ "clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "peer": true + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" }, "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==", - "peer": true, "requires": { "color-name": "1.1.3" } @@ -2561,8 +2458,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "peer": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "combined-stream": { "version": "1.0.8", @@ -2575,14 +2471,12 @@ "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==", - "peer": true + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "peer": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -2624,7 +2518,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "peer": true, "requires": { "is-arrayish": "^0.2.1" } @@ -2632,14 +2525,12 @@ "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "peer": true + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "peer": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "follow-redirects": { "version": "1.15.2", @@ -2659,14 +2550,12 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "peer": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "peer": true, "requires": { "function-bind": "^1.1.1" } @@ -2674,8 +2563,7 @@ "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==", - "peer": true + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "highlight-words": { "version": "1.2.1", @@ -2686,7 +2574,6 @@ "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==", - "peer": true, "requires": { "react-is": "^16.7.0" } @@ -2711,7 +2598,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "peer": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2728,14 +2614,12 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "peer": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "peer": true, "requires": { "has": "^1.0.3" } @@ -2753,14 +2637,12 @@ "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==", - "peer": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "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==", - "peer": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "loose-envify": { "version": "1.4.0", @@ -2833,7 +2715,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==", - "peer": true, "requires": { "callsites": "^3.0.0" } @@ -2842,7 +2723,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "peer": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -2853,14 +2733,12 @@ "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==", - "peer": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "peer": true + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "picocolors": { "version": "1.0.0", @@ -2987,7 +2865,6 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "peer": true, "requires": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -2997,8 +2874,7 @@ "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "peer": true + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, "scheduler": { "version": "0.23.0", @@ -3011,8 +2887,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "peer": true + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" }, "source-map-js": { "version": "1.0.2", @@ -3030,14 +2905,12 @@ "stylis": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==", - "peer": true + "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "peer": true, "requires": { "has-flag": "^3.0.0" } @@ -3045,8 +2918,7 @@ "supports-preserve-symlinks-flag": { "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==", - "peer": true + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "swr": { "version": "2.0.4", @@ -3059,8 +2931,7 @@ "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==", - "peer": true + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "tslib": { "version": "2.5.0", @@ -3105,8 +2976,7 @@ "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "peer": true + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" } } } diff --git a/frontend/package.json b/frontend/package.json index 491a69aa..cd0b6836 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,10 @@ "lint": "next lint" }, "dependencies": { + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@mui/icons-material": "^5.11.11", + "@mui/material": "^5.11.15", "@types/node": "18.14.2", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", diff --git a/frontend/pages/admin/data/tours/index.tsx b/frontend/pages/admin/data/tours/index.tsx index a435df3b..1738f049 100644 --- a/frontend/pages/admin/data/tours/index.tsx +++ b/frontend/pages/admin/data/tours/index.tsx @@ -1,24 +1,52 @@ import BaseHeader from "@/components/header/BaseHeader"; -import React, {useEffect, useState} from "react"; +import React, {useEffect, useMemo, useState} from "react"; import {getAllTours, Tour} from "@/lib/tour"; import {Region, getAllRegions} from "@/lib/region"; import {withAuthorisation} from "@/components/with-authorisation"; import {useRouter} from "next/router"; +import MaterialReactTable, {type MRT_ColumnDef} from 'material-react-table'; +import {Box, IconButton} from "@mui/material"; +import {Delete, Edit} from "@mui/icons-material"; + +type TourView = { + name: string, + region: string, + last_modified: string, + tour_id : number +} function AdminDataTours() { const router = useRouter(); const [allTours, setAllTours] = useState([]); - const [tours, setTours] = useState([]); const [regions, setRegions] = useState([]); - const defaultOption = "---"; - const [selected, setSelected] = useState(defaultOption); - const [searchInput, setSearchInput] = useState(""); + const [tourViews, setTourViews] = useState([]); + + const columns = useMemo[]>( + () => [ + { + accessorKey: 'name', //access nested data with dot notation + header: 'Naam', + }, + { + accessorKey: 'region', + header: 'Regio', + }, + { + accessorKey: 'last_modified', //normal accessorKey + header: 'Laatst aangepast', + }, + { + accessorKey: 'tour_id', //normal accessorKey + header: 'tour_id', + } + ], + [], + ); // On refresh, get all the tours & regions useEffect(() => { getAllTours().then(res => { const tours: Tour[] = res.data; - setTours(tours); setAllTours(tours); }, err => { console.error(err); @@ -31,18 +59,21 @@ function AdminDataTours() { }) }, []); - // Search in tours when the input changes - useEffect(() => { - searchTours(); - }, [searchInput]); - - // Filter the regions when the selected region changes useEffect(() => { - filterRegions(); - }, [selected]); + const tourViews: TourView[] = allTours.map((tour: Tour) => { + const tourView: TourView = { + name: tour.name, + region: getRegionName(tour.region), + last_modified: (new Date(tour.modified_at)).toLocaleString(), + tour_id : tour.id, + }; + return tourView; + }); + setTourViews(tourViews); + }, [allTours, regions]); // Get the name of a region - function getRegioName(regionId: number): string { + function getRegionName(regionId: number): string { const region: Region | undefined = regions.find((region: Region) => region.id === regionId); if (region) { return region.region; @@ -50,73 +81,32 @@ function AdminDataTours() { return ""; } - // Filter tours based on the region - function filterRegions() { - if (selected === defaultOption) { - setTours(allTours); - return; - } - const selectedRegion: Region | undefined = regions.find((region: Region) => region.region === selected); - if (!selectedRegion) { - return; - } - const regionId: number = selectedRegion.id; - const toursInRegion = allTours.filter((tour: Tour) => tour.region === regionId); - setTours(toursInRegion); - } - - // Search based on the name of a tour - function searchTours() { - const isDefault = selected === defaultOption; - const searchTours = allTours.filter((tour: Tour) => { - const isIncluded = tour.name.toLowerCase().includes(searchInput.toLowerCase()); - const isInRegion = tour.region === regions.find((region: Region) => region.region === selected)?.id; - return isIncluded && (isDefault || isInRegion); - }); - setTours(searchTours); + async function routeToEditView(tourView: TourView) { + await router.push( + { + pathname: `${router.pathname}/edit`, + query: {tour: tourView.tour_id}, + }); } return ( <> -
- ) => { - setSearchInput(e.target.value); - }}> - -
- - - - - - - - - - { - tours.map((tour: Tour, index) => { - return ( - router.push({ - pathname: "/admin/data/tours/edit", - query: {tour: tour.id} - })}> - - - - - ); - }) - } - -
NaamRegioLaatste aanpassing
{tour.name}{getRegioName(tour.region)}{(new Date(tour.modified_at)).toLocaleString()}
+

Rondes

+ ({ + onClick: () => { + const tourView : TourView = row.original; + routeToEditView(tourView).then(); + }, + sx: { + cursor: 'pointer', // change cursor type when hovering over table row + }, + })} + // Don't show the tour_id + enableHiding={false} + initialState={{ columnVisibility: { tour_id: false } }} + columns={columns} data={tourViews}/>

https://www.figma.com/proto/9yLULhNn8b8SlsWlOnRSpm/SeLab2-mockup?node-id=68-429&scaling=contain&page-id=0%3A1&starting-point-node-id=118%3A1486

diff --git a/frontend/pages/syndic/building.tsx b/frontend/pages/syndic/building.tsx index 7a48cba9..4616871e 100644 --- a/frontend/pages/syndic/building.tsx +++ b/frontend/pages/syndic/building.tsx @@ -2,7 +2,7 @@ import BaseHeader from "@/components/header/BaseHeader"; import { BuildingInterface, getBuildingsFromOwner } from "@/lib/building"; import router from "next/router"; import { useEffect, useState } from "react"; -import { withAuthorisation } from "@/components/withAuthorisation"; +import { withAuthorisation } from "@/components/with-authorisation"; import SyndicDashboard from "@/pages/syndic/dashboard"; import { AxiosResponse } from "axios"; diff --git a/frontend/pages/syndic/dashboard.tsx b/frontend/pages/syndic/dashboard.tsx index f764fc9d..b9e4cecb 100644 --- a/frontend/pages/syndic/dashboard.tsx +++ b/frontend/pages/syndic/dashboard.tsx @@ -110,7 +110,7 @@ function SyndicDashboard() {

Bus: {building?.bus}

Client id: {building?.client_id}

Duration: {"" + building?.duration}

-

Region: {building?.region_id}

+

Region: {building?.region}

Public id: {building?.public_id}

From fafe457d9b1ce4bdf6e40386fe65e7f238d8f44a Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Wed, 29 Mar 2023 20:15:37 +0200 Subject: [PATCH 0385/1000] URL to retrieve all buildings with given region (fixes #207) --- backend/building/urls.py | 2 ++ backend/building/views.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/building/urls.py b/backend/building/urls.py index 4ecf5435..524158c2 100644 --- a/backend/building/urls.py +++ b/backend/building/urls.py @@ -7,11 +7,13 @@ DefaultBuilding, BuildingPublicView, BuildingNewPublicId, + AllBuildingsInRegionView, ) urlpatterns = [ path("/", BuildingIndividualView.as_view()), path("all/", AllBuildingsView.as_view()), + path("region//", AllBuildingsInRegionView.as_view()), path("owner//", BuildingOwnerView.as_view()), path("public//", BuildingPublicView.as_view()), path("new-public-id//", BuildingNewPublicId.as_view()), diff --git a/backend/building/views.py b/backend/building/views.py index a227c77f..40ce2a9a 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -1,14 +1,12 @@ from drf_spectacular.utils import extend_schema -from rest_framework import permissions from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.permissions import ReadOnlyOwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent from base.models import Building +from base.permissions import ReadOnlyOwnerOfBuilding, IsAdmin, IsSuperStudent, ReadOnlyStudent from base.serializers import BuildingSerializer from util.request_response_util import * - TRANSLATE = {"syndic": "syndic_id"} @@ -150,6 +148,21 @@ def get(self, request): return get_success(serializer) +class AllBuildingsInRegionView(APIView): + permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] + serializer_class = BuildingSerializer + + @extend_schema(responses=get_docs(BuildingSerializer)) + def get(self, request, region_id): + """ + Get all buildings in region with given id + """ + building_instances = Building.objects.filter(region_id=region_id) + + serializer = BuildingSerializer(building_instances, many=True) + return get_success(serializer) + + class BuildingOwnerView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerOfBuilding] serializer_class = BuildingSerializer From b46458f796f8e17244b6960ac95c0def84606122 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Wed, 29 Mar 2023 20:20:57 +0200 Subject: [PATCH 0386/1000] Use correct serializer for documentation in AllBuildingsOnTourView (#201) --- backend/tour/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tour/views.py b/backend/tour/views.py index 94689dea..3b465462 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -84,9 +84,9 @@ def delete(self, request, tour_id): class AllBuildingsOnTourView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] - serializer_class = TourSerializer + serializer_class = BuildingSerializer - @extend_schema(responses=get_docs(TourSerializer)) + @extend_schema(responses=get_docs(BuildingSerializer)) def get(self, request, tour_id): """ Get all buildings on a tour with given id From ac4af7432040e2b3209785c05c750aa04531c27e Mon Sep 17 00:00:00 2001 From: Emma N Date: Wed, 29 Mar 2023 21:40:34 +0200 Subject: [PATCH 0387/1000] Filter header done --- .../components/header/AdminFilterHeader.tsx | 130 ++++++++++-------- .../components/header/RoleHeader.module.css | 40 +++++- 2 files changed, 108 insertions(+), 62 deletions(-) diff --git a/frontend/components/header/AdminFilterHeader.tsx b/frontend/components/header/AdminFilterHeader.tsx index 0a216c1b..5a736496 100644 --- a/frontend/components/header/AdminFilterHeader.tsx +++ b/frontend/components/header/AdminFilterHeader.tsx @@ -8,54 +8,40 @@ import menu from "@/public/icons/menu.svg"; const AdminFilterHeader = () => { return (

- ); }; diff --git a/frontend/components/header/RoleHeader.module.css b/frontend/components/header/RoleHeader.module.css index a65bb99d..b5035866 100644 --- a/frontend/components/header/RoleHeader.module.css +++ b/frontend/components/header/RoleHeader.module.css @@ -27,18 +27,48 @@ color: transparent; } +@media (min-width: 992px) { + .menuIcon { + display: none; + } +} + +@media (max-width: 992px) { + .menuIcon { + padding-left: 10px; + } +} + .grid { - display: grid; - grid-template-rows: auto auto; - grid-template-columns: 1fr; + display: grid; + grid-template-rows: auto auto; + grid-template-columns: 1fr; } .toplevel { - grid-template-columns: auto auto; + vertical-align: middle; } .input { - max-width: 350px; + width: 250px; padding: 5px; margin: 5px; } + +@media (max-width: 992px) { + .navigation_links_top { + display: none; + } +} + +@media (min-width: 992px) { + .navigation_links_bottom { + display: none; + } +} + +.profile { + position: absolute; + top: 100%; + right: 0; +} From 927b71bb7787331e68f5b94375cf578fd3c80d6b Mon Sep 17 00:00:00 2001 From: Emma N Date: Wed, 29 Mar 2023 21:48:47 +0200 Subject: [PATCH 0388/1000] fix profile dropdown --- frontend/components/header/AdminFilterHeader.tsx | 2 +- frontend/components/header/RoleHeader.module.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/components/header/AdminFilterHeader.tsx b/frontend/components/header/AdminFilterHeader.tsx index 5a736496..60444aad 100644 --- a/frontend/components/header/AdminFilterHeader.tsx +++ b/frontend/components/header/AdminFilterHeader.tsx @@ -56,7 +56,7 @@ const AdminFilterHeader = () => { />

Jfegeodga4yutiTR|4sW4^w=+%X<4Hc#Yn94u~G?;CKgz0c0$JLU_G;Bi1; zDK}G2mo>K?vQd%VRmCpJ?gxpmb;64n#aXf>ve|*MNPtM9f<+&c@I0A-fbU#PIJH;h zoj0}fGk413gvc?On+Z$%6-e?3l3V+dyu9w} zO^?`)*>^)A+=D7LnTJN)i(AOG=4b3#^@*pY8&z;%5wc=7z=7lHYbrA()c zPhAJo2i#IG@JGtkc$P3f>beWf?QqudngHmN&_R>E-xIK}DSbTYt7t-w?qR zy(ciR5{dJsYzi`#`<~q^ zE{qjCN#P4wa$3&P1f1QU8IR6(#gBi8riDKJbF&gYE2VtzGN4zM zH_;fIuNuppRLg9?`n}MM$5LA@w4tqLtKWm zB{|GHi=8e9am>sl4<#Thn|50lCW&3s&E2q<)oVn)o5Oysi5^R_SiS=(W}Hod)rlb_=HQLCs62hFWzYvue+nAl_5sDPc~k(pPB<2Gx^TI{=i{j>ME+HJUpe=Uiqgq9@Ni# zYElAGXt7j3a=YbaTMV-bw7CL7B!^*4V!qWa}Nr#(BPib;$i3-MUJDCI>8Bu;C48a1$e^*Mxn*nQ!L; zlG_UR4rwCK0`+g@{mf*r8|n3|z&^dJ7iup0mVZO_YTu<8AaHuDV0z=Ug1Ra^K**ZK zE@M@)W4+qhZ}H{y)g_xEEyJ}1zr_x{9$ry^Gic#Cv%Jf54Q^2Mn7{?Kc)fbZTPfkS zpzgR!!jwPmz8eDLMnN;F&nZ1aRkRFPT$$GA#Xks$I`&Iqma}roLkm13BCfYpc&+k_ z0dnd#AZB)aM947%Jv}C95Ok0>v+E+hsbII?t~0{%UP$ zRS`e^wpRb4?EB$qF!(dpgKz6a4PMflr8|^Uwpb$DJ1;szg7(UN+S0G<5M%GJf|H9@xN!8|A)^G-(o3>_xt*oa;B4v%Guqi@@X5!55@6X@@=yy>6$ z@E5_!E1g$=-}~>sunNJG=MB9Z0;}1L{a1|kFWbA3dNdAQhC0n~>LBB4>J8R=wFgXz z2oU+jZax8p5hoKGHd;#uvUj#u|g~N)AqqeZk74k5kskE+o0tveVGmu&M3p6){Wr8?wUHLv)XgZ@rBg(Rlm~oVfzy5P7d&aLyh&}>%usiV(z412jUT_ z{;LQHGD6~9&*dQRp!~})&e!{101XT@CRJ*fvOh9(R&&FiH0Ho^8@@6)g{KStc@fZ0 z$ED~^=8`8j>436l1v^}7Sj|yUuAdW&fhVaZkiC-=C4l#D@&C1t&G(pyl^pmiRq1n! zg3JwK2JXF!0`Gf6W1Ep~Ma-n(7O1>(7Q6ug4x+GI5PU-@R5;}r2#1kC3ZK&u>3A&5 z-=U-?b?ia}_4V~d+83*utl*)tY<(viX1t6?5*|DWEa%Y3TlrcI6e<0B-iZnUB4T}u zu~IeiE5}unhcnW}DGxi}-`hsLUuRzKB=Uua9*-PiQ5`e8`&vuC2xQrg!1K&=itM~7 z#HG~YR=bO-+E1eGhOOfuAmk^?KZf~nrzxSRI_ z@-f{Vbgi>~DEvWMB}Hnl&fU||z~71AvB=t>6As{KW*Qo%)P@wAZ?L3xe2?h2T!h7m zV2SxE(So(2f+g_Xem`-<^e8v%gS>IJbV4krOXHMk{Gb!)`Oag4ly!IFRJxoAor28u zsI0b}+$L6FN8+6Ccz#@{AX|FO26n&YDFv(RIg?*h+v>%$8r~(nE%yWb&;fmn^Q7x> zmXt6VKlU51J37Bc|NObe@&2YrNjmM%nes^UZELqAxkgD7ju)ei&dkcL$?FVEHH8$e zM6S21Y?FxbL!r}@-JQaD)?izA+I~JRceQNn@WvB>Ieq!>JZe_Rn0O}=`hsaD>YpFIp2Yh~0TKD(;;# zIq;+nbvx8kp2JNi;K%?H$Esrh@VB4gzo(Ik=3#YW0EXUz8Q-m=Vf*UAGWww6M5D@G zRG`yEIEf0Kz1$$IG;15)wVe@$0Gf5lHXa7O&IQM-7mo^O77hS6S~uA6UEqOVlw3hM zhMKmCx`zgBZMNtd@v70&sJA*QxjfD(-fzT_DIw(oD@<59Wl+$-QPcVe?cxtcM)06| zX@BP-=Z)#K9^*FWQMR*FSb7(8V2n^pTUO0e08E6SC<85;<;^};in)A=<_-IZXd2_T znaQJz7T#lVD)c90P+i@*VQ_;hvOP0NfL-V;qF0i^dMptgQP41jO-(?xCr$Hw^G?}b zt2c`?wuCs6kUu3SDO7YXp^Mlswln2Nz?2C8(4D%`ZsF<18vyD?J5ztSEo20qnRGT) zRnHsz#su@#wj4r0QZm~WV1!9G7`Zh@ZBKib!qviVg9nGZt92wyMnW!5kJbUb9?cIz zcQ%xiU_cp%6srOV!nazxwwI?K*lWpIwMs-?$?MPSB7HGF-C)CwI!y^AV(Q)F5~quI z+RDj^Vl}OsRJRuJmGosLQ+Rp1gEqK#uD|(4bQHYa__COA|gUE@J znlo?V8*cTn6yG=Rh8>?rxAingX|b5fxV!!$_%W^X{#F@shbeQDPX1>>_%8xU?41~Y zO-ubBw8|YU*HKY$*c=V8it!f#-~CSn31@@ti@qkqzG9}|rE~f~zX*cWem})~N5*>f z&#!QO;H6_(0cFVK)>&j-;MpfYRfFn!sHJxsPhJPWqlH|NY3%xA^&9)F%ZX>Css2x# z&{ox3??yIx#_yu+K3?vV(5OAT{838qK$^8eTp$CzBkESN;Qr*EQCKMHOdwkB%aIzQ z0pm{ycs~8$8d6yQ(DzyWs)w{pZ0GXwmfa>ra%9VNMZV(aw$h+!jzV|AMW#NNtKE7o zw>?Zqv^&>5sTu~{)#Ch*o1bs00y9_Z@32qMm_s`1L{ywq{9q3r zu8{DsG|dbeQ770yuVBSX$VA$w^XXNv-fvPCE>PuglDwB~D%=Ph9M6snpT^rG?nQwJxX%6ssf=b8Tr(_%Pq_e`~%Z{4gM-yQ98&ZkPKs21)%b zPR=&3$1tpt#?G5ULa5zUs~~PgL7UEc^=|?jG1{*bqk}`S?Vc6PMdSCdI~6_MTN#kO za<%Tda1K&!bux{QDt0+?DMv9#P}z9FNQS;-;x`XzSD=2T@9n55V=APC?oy3%Rquyu zG&?BbdMhL`fL}Gl&iDb_n?E(_O|4`S?B27&XLmvQB7#YWywB7VE5{I1=PJ2Uhio1C zK-W>`-$qpT<98CUmhTLNVpF`H=*JRZ{Pu-Zl`#q&U;myLVMiC0MXlsPsJs=R7zo+| zFzgA)yd_Assa;Mlnd;~0SY}V#T0hduUm3&%-(YS=%RJOQ0c!3h#>!>qmUY5km}9Fz zl(%i&7CJ3%SJBrnArP1CER>?bLWHa>;|6P>&YIcW=W@RYGO)<%nS~8+svSg9>n!3Q zlj|L187Ce z-Yy<3XTy%_T)IoJIRljIrXoAiXtEP`1b9dXhi%>7$R0URNw=wjzpHz zoI}&<@s4n=O#J!|VfrXAr~jRg|BLc8deWt~nlc#V(U*_;Y_4|o3Cya%*YSvmItBoV z$75oG2iO_@5gX1_wWuyl$70=y5okZx$%c7_q8U*(i$G8mOr@op|3zSdp&i88J$4+v zBSem4ww-Lu>`-%2sg^wyM8Ave+oNlMK?#9`Pxl#fu3?wymof7u@RO*MCAt?1`|b{3 zt}5NOE_i&;51e+y?loEzjOhuc&_rb}g3GH4(5VKy*e;{6_GfZxmc-P3vEDJzSAw4e zUf1-hm)hI!i*HMH=HQ^^X(Cz^#ZjT;R?+;_g_5wh!qp-`0S#I%l23{&W z_|~|_K_0pN;6z#{AFrT&dKymlU%9C$UsDtrS`>`diFneuWiEp3kh=Y8#ar543gLou z@@h%Mex zd9!HBHZw9X$oPH##0B?xKiVwnU~+pTu67mY0Bd_C#mAud`_tr?=0cQ$FibBO-r1N+ zP8FKZjwhJo7vsAj-tWCE+CrrTVpbJlYVN8Dotd)SM7)l9=M?v9qlwQoE<2_cu?R;h z)@M`H(`L@K8kDF#us#@KdTa%HUiki=_&Yn#4s~g(&0Exo9Ap*x2EJhRS@~(S)D9i2 zytmC&uo~Cpc$#5|Ewe&IDh`#lqX5$04-B>3`A)d&1gx6Gn3R+oY%$HZp#-Y91xp3$ z;_6ym@B0!7kBvVknF?_ROA!kP7#>HQ(U0h&r9WoT2K*S|mdZ+Mg_KivJXo$j9L5FiK`TsKxX15X?R zg26^RBt?VJ*duBe9_E^FGXk}v5|-Omn`AN)f6Zhk5NJ;1T zcrzpmyhDSZa<8|E$3;wTNGm6&X_abfUdAsZ2o8wCdooas$@{90Q1r&0O++Q=Osw{WNxz zo3UURq7Cf`TPj8KY-fL-?<@+NR-W3L@xTD&cet^iQl^vY#8PaHJ$xC*ritWSV0bw| zuNB=@k402W+^cuSZWep>7o`?!Y;u|{^b3x6`l9S2l~kAcUy?VEhe}dC{@lJ%y21aT z!o=B_p_kT%He@>UIw&-s5^UpmD-LYJj7WsbG7|}LOOPLIi;p*-a4n3me6@8skf`W_ zw69wzM!xCrw$qi~j3e7hLO0xZw)7UB(rB+{<}b&5Okp>mA^*1Pav?aM^|1F13v-Kf z)?9%CSlWJVA&UN3!9PTG>Da^jw+-ZV+SA2C_TOVCgbc4O0$z7~Qj1&Kj~j+gRLI+= z92(-}L-4-!Dhc%uJ0k+^3tvxc_zoaWB<_xDk2=~DcTLHe_lBL1ebBnkOi?t1>gQwx zx(R58Qam4zs&lk+=Ts`&(_r4PVd^&#P4PIY@#q#RKXs0+rpjyfgfWO{{bVKg`$<)^ zNB6OTUjbIUoBlR^xS50$)5g*se)~myKFYUZrH2d_$kv_Emq5a-0Esk4W|XxZvndnneY-s7dwfQrtezCL z?yOnh>@2h10)-@Xc5bYd0MiaaVEXvQ!YAuTe$sTAUk`lF6{W`VXT~RZ>5;1`La1y* z%4DA@J-P=|3JrxmJLDE}Xj`tasGTxX(gwD=jqRNGR z#Se;F0%FG?7v2pCQ@lZ{kY@&RWK9&2Bq^iVyzVT2-|kSGxoU^Kc+0vRHjf<{Q0UP3 zqTu7SU}FOs67!c+Du|#V2#L#CV^+<{OX`sS!m0oNQeOTKMf`LfONKj0EoZVEUZz3@u-(;r3N)T$V(t}rMZPIod(sT;_E;FU1E#U5rO8B>1| zD3ZjcK1c;z9pvvkWhZ?eN;3)0#K%_!iY|srA^wo{vD{*yXB`%X!p2S-H?0C=p#`t` zj>jyI!*t1f5g`o`A#=_ti)+Tjt?%w9=s@oSyW@`>V`-_$NoXb5@Usb4ctH-{MBS5jha;~U;g+UF&2i$>UJKlga3Zd_p#f`_?B z=+C=zhP5XiWO7s!1_QOfj={@66x}!|aBxjI=CGJekv} zd`#se7~%pSnX72ew!m9|h_v3}JLX|JY%o(n@mTAPiflFilQfc4S_>1zqhgQ5t}Ow| z?2m&k{?z>iwXuo`ymUQgtclD=h8OJWhZBg_2G3S6awTJ_03kXZr)~b2FkiQF3!xRH zz3^LoOjOHkU=`58-YR@9t$1g0d#L;N`#S|p^>K?r!Ve1als%bHGjM1PH|q~L$c6tj z=H%~neNfaGR4MKCgyNkZSX$Mi+Gy`UhBwm2<1(>w}&zH{Auz8;^+)#gfRYBhFJA%woEhC4-lGjiog+MW zJ@_g3>*IO1gu9|yP@4OG zQ}6iZF9MUor>6S92u>Zl=B^8`E)J_HI_j4etcj6RAo;kr85P>FjAQoB*Epaf^EN;< zK*p1Z58|jYDF0Qk!m?}N;yLb$R^9pSX<1wtG!s4C!Ugia2A8RxN`53Pp8G|>qrvry z;Ce+o>ul>60U(Tr^E|5oYRI+)yL8=SwRFm~-{UA1c6*?6ByFN(th%YJ9&+Le$y^=z z3h`;`pvsU|6-3H(4Q@U|ol8djeroTi&Y3NbDVez{Wa)I9Q-abbJ-OW0~Um5J4xIY2#rr9V1^ix~Flwhd{0e{0?=vTQQ@n9?SXC_AJY*X!B$AtXL& zuGW-#oO$l8lVnlh;&`^hiTM=$qh5Tl=4!2daqD3}l($kC3geR+k#?t{5PbhRxCQatJObhv)&QwVmkR8JwB3I9G= zGY`c^Ldw3e=q-%u@b4S-Y;%aGdo}l>Jeh~VbnmX7SAqSSc5D$cicGbsuPbD3VQfUt z9`%@%&I-lB!hms4s%=LguX?&eL7S<1+B?S2=)IeFjr-Y_;q-g&_HLy0S@c_ES^kFE zlws0u;$Y0%1TBy+(^9E2$|&X?f?N*IP8j$MNsW@!@S^LCNyXyy^dTcKZZIz%{OG3R zzA4p^3E1)-fXBM{GI!4(dU71`dk5= z(?&ub78g3H0>(gdiQoUrNAX`b{tpex7Li;|0X#Vw5f*|&+a+8R+VB_{f0BL-CftJF z3Piycy&(4Z#uL24HZ6@j{}YSEbuEC*I7=b(^uii9ij}A|E}NYNyd6m-OO3tTY;|}Xc?tqy@dN7p=(949;2b-~bN_(mMp?uqJ5q=L z;EwA;^h2+)3QOo$gY(DBnH!+I*}rYA$L% zwu9viVK2VRX$8nP>I5o_beLu{7qg@nhFYKd0Tnk#fmt33AJ_>FBT{qlg5~gx3&I zs#>9w*x-oT zD}VJ~MbWN7++w1dH>STHnFs_hEBFSi_}d7wE1+)|{l`57^neP4=X2wk-9tM9?(TAV zw6kGmlPd5Q)G-ynw)Y2=nWboV~j~p%khyCZvEd>SUMw5P@|0Zuh{))#(=(c!3&@lE-TzUgKcy0E^m0?za(h_0#q=4DO-wSl#* z!)O=Z!Dj4lzX-k;Rcl`T_$vwRe;X*Z5(dp~LM zmIt`%Ahcd}7ZnN#qcEIWZ_!pK@>`-GAb4JSUS3hem)}@YH}x!|3v!Jd_3hDhU;D?o z7+yjeEai52;~8Q}>}&Jc6cQ1}+J`U0Yz(ht%!-EE$R zqkN}%dL_T4HwS3fTgLZ7ZS$F{m2OV_sagIhDb1Ny;^S7KJ}YbY5ZQuzS;>sz>uanHHu>h!#C}*4J&?=r zL~edIv0r9;RZEDM3v{IC(*xD3slJ49N|fH`?GWylxp6%9ZlrBl?L!8Y;?Hpyess>A zq|siieDGyod;W^!#+haN;KTUA6IB|wuZD435e8(6&He)LcUb>qAKpEIPse=IVG$Xd zL9~EuyYJaqQV*FfRnUp#Kt{5SsjyV5WztL}6FJehqQ3mRXF~`EI{k+Qy@Px+;)tW2 z_vD{gIK~_R?aS~^jVq%7TydA-i=qXtV_rQMirq#PFrPig9#LaZ!7RM9Vn{ zYUxzNe~)o|V!o3znLwYG%=*}1)R7e%?BeLaK(`yEYP@wF%6`1tD$o0*aREZI=@_&i z9(eEex=gg1G53d7?}ujTQRU~Seagm-w!Sx!H=V;bSF)fYwhOvZlV+ZR0n4yxmJf~i zclC%f$LFjIrj8u%`D=dZo2=e)tjLBjr<)=geGR26ehX3_M6%als`o)L3BMU z%!r?VS4(&!xd`O6qqLC*D9n0WoSZ-4_`C6f{>Nbs^*Zin@D})9W4LN$vUI}&+btfXq*yP>v!7B0zI>j!%JibUbWP6 zy5BuREVMxi8jTeTJ~N%b;l6_B35l*I^?i(s|GlI_l4^W7KNi^J<4+@SLzrH-2}^BvhR#hSsCJ3{6gT{ z^5o-BqqBRdGiHZhR#5;pf+4>7%pDV-qFrA0VDj+of|CnLvdIr%sYtyI#R15j7?j|_ zux_R}Wa39s?G5xT05e-R)+Y}yWznucxc%?0m|eSg6#7CZk4AI-3f zY?+ItY3X%KRA!EkrSyjNHSnHZ%PZp9@@U8K5H~!Wqd-mU^Q>yw7fiTIIT0X*EqYd- zI1^5V@+7ggPlx1E3^IOBeE9a$`2CrWM`{pqQVSf)7{EgkS8fjrB%zEc*5#C8EkScZ z*Mo!ym{VUa6fQSHEScE?TmnFdtrF;#3UI8VrSWd6qml{&r;#O2SpAhh)w?*kpkPcT zBeKVn)Fx}ktu~HQm=(_%Wz_cM;>6f}XJq6hD~gB@0IS7&eI~dexIlZr@@WK?+0GB} zxJrlug=3z^D={`-!dPd&1=Fh+sM;fKwxHq)Hg*b1P;ZwTsy8^!sIaCp<|VDu(|U(5 zeIhq_cdABBckDUOpX2pbtUQZPQ+t2x7)L3FXia`XO&8>{v%WncICC+&E=>!Y@RQSDsMH zd09)4@Rzcrm5!dB=xh&;V|aI=Idb?Pz#GZrqlk?zPEN73Kv+Hw*N{2pG*h!01a zHRWW+Hd0=-&HUvA+P=@%F;<|<4xXoLTBq9EO_~X!tpg&sZQuw!*d%FjUkqXew1r?0 z5D=I?e_zuX)h@cN5Y|I!@sSyYleLX!3daK=-jX~%zNgncn{&NMqjSBo(3%~vvUJtly`ZT6;)T#%f(C$NI^6bx6sb!@}VG3^SMu3f9)dobQ{lw zr@*VT*0*Hl1BL!R`q>@!SEs({e}!lL-mAmI;63o3Q2%ndqr3GJ*KG3LFWRiUX7rl& z{9v2aarIt7o+2|P44itNA{;1P-*e0&d5R<7wl~guQf}Y2%e&X@sZ09(S;||dp8Y_U z3c2`&$9XOHtdB*Q1I5u7_f+LBU%wKpe`u##bZVjG7|lLA@A#CTO1@iAyr-FbBt*K% zk%*ZdE?o@PR{kM)^LGAOJ~4~uL!DcJXQAcll?+|+8{PteKI&<4pUPQBA4*T@Fjh+! zyN2S9`00KBWi<#0p7(>Wif^7iPAe`j-cj}CDrI9&b4k@+hXrz-B|?4ExKV+Pn?cIm zAKAL&E3`A+0_4r7CcFBDO8O}})kXpt1V|~TQd0(7e!mCFXUTdvGwVKRF6|PFAQ~=x zGH*s%UeFjAGv1+d%dDm=Z&F1UU6df1OKtF*Hw81Nk&fc(p_>?-5kwjhRsYBN5ptJQ z^On|haewYW)EX(}rElGowXgV@NN%^ix56;-8>8Xr?_Dp|9tmUwSkz~r-iEd*Bo*sP zH9X?$+ozg2-4;y(XQ^CzdTohNzzX5BnYxO#Sf2~34)Y!cmf40{Ga?&2ikw-Ko5_!P z#1X5m@J-N6X2U~!R!2|k(}=pB^wU!4N3)C;8s?Ix@7>#=oEUFlF?T!ksM7aIT;$8?k|!n0#iiH?=I5pC#}+G@gn*7(Lu`2g zE^bO?ooK#>VH#n>QrAtlv~aO5YNo4li~y?`*xCT0>5OO3x$;jho?SmmU}^1d9tfJ$ zjqxeG`$-=^yWtfAWTM)3Vmq5+&f*n!*SHdycCk)n(wdhrvN3U>^lY3__C-zQycss2 zX-jxitESC`wNe&OpR3fu<4eD^RYfrn`#(QjB7fWmzF`oOmz_v(*~u;N8JYF&YEZ=viKhPdY&|DL95-a`o_IUqfK9HRcNNxQA8h# z@kYhlV<>Vud60h07{FckI0vj7$KBoCn;n}}S3COMW;9^hD*2C~iLzR$?ag#U1iK=Je4MpiY< z`|YP9^4$%h9>u9S(U=IgX{gobKpe;Rdnk^?9wTt70`QI}kzVX@4bm9&?R{u>!X90g zv=(1Zr=6zcMK6=^2%H<6&vwqtqyWRq9)&1I)A!a#+Ah8!PpnT&r%Ffi>&7^4A#ahK ztB)N#6Z9yV=w6|4E{op2r2zNwFh3n_eZ12#;L0Em`k83zjP_9QDaF*PZ9M?VP53~s6AGzkPh6Rr*q z+K{CeI_=Dp3o%jdA;AtCThb+m@)Vs&@6?iS2pH*sbHPvazY5t_>_lrE-7Ic4m5KPC zCjl;OQsR0Jh~+T$UQwLYpYHYhxCnzLJr3YsnS>^f$s`XAbnKUSB;p5)X{7VLR_XZQ zjrRDs^9L=R3KrO4Q|B%EE2*N;=+)th{VT!u>nf$_WZy1+HHxLYGdhuxrok>*7xsHA zo;5p3p)371DsFhayFcleI_ejcPyZ+>&mFTl?-ln>qH>_b-o5ov7Z+Dx9L|6C02j^< z%|mD_Pe@xHSIfn#C2_XbZM%ZbUVW2^)Oqut+x(`^pZ+()i^Tm&f#aX{W;S}|#aDBN zpJ2H!^D_hNzl%q=Iufsq%=7rJd@}cM6T^pF&HhU}+TX6*jj^pZ%{YU^ne_*r`5;IU zKTYpv2w7o1j{$}8io6QLW_7r%`ttgK@|(Cr&9`qcJ{QZ3cLQU3eqY*zC2sqU!ZSB2 zkx|vwYR<{H=i&*t?R{NzQA-oErmy?2Sz~l(M!SWc2-xJ;u}B0ZQ0nw7 z!r)TzO3GD-{QR`I)@8g6?>G_)FZC!F|G4n>RH26O4NCwPK)nc_DsLrEiFRC0}L{_yX2qX zKEp6DxDD>|y{=DZv%`A2X;aj1)(ka;M0>eCfmE-$3AYg;w~D zH_!O^=RbCgvl3_lA3F;1`WR+oZ9}0Cbp!~+Q~IKjJ@1E#yMcWCjEuxNIH17D?X9W> z5P#9Ttm$Mcti7p)?y!g8Bd=5NcKlp5YgboDNH(!NTo|o`oFteIQ<=yk_GU z|BMKAqqBBW@J$j*%f40Ivex4gy>@HDosHFrCS<*g0qG0bjZP z6z!x6RuVBb@suTaaPJo%7a8;Lyr4zywr%#v@9hj_D6X`1X^YMdkh7^PyH|SbL5*-J}FnlK3d`}wl-C%uCICb zs98pBgVWq3+VC8xIy|sRfC&ZdEK6U6jAbdmW~9q~=&_{^4JiEI8B3zjqkS zQWPe0xr1G9YL5Imx0t$W_aRX`1m0BO*GVt%8_xvzN!Fj>CWh3weHo?28>G?Tn*ecj z(Twox73wi{{9u1NAPj-;Ns^GXj?e&`_}}~pi9mN~Zhr6h`93GA^lYGi8hX;J z5f6xRIL@l=uhgM>q2Udlcx=8?n&!38O?%&`0?RcSTL+_nv4&@Ohdg-ZwJXNYADoeJ zufz~d$7V2S+mlGvLf7;rY6Rp3+a=8nxrpaeh_;g>8WFqGm9;@zv>bv9yh`rWSM1n1 zc7arFReg)FQibOXpx(|)LEP>2v%oJ9%h z;E3-o*KTHG5q+&?x>5n9N&*d}-_pEm;uO^>IlJ$BCx=RZe!akdl5dwd;ZOQv^3%Uk zqW_dQkiWn1)2Lt8Ryo#@#qzq0!rA#Oga6!rXSSh*ph zJ_<$FLHN0IR$O->wYFQ66PvNWP6`x4xJ9cekyoxn9HrsbO7p~=J|YnENt70j(%4W| zW>u;Jc>j4WpTm{c6!8HUlL(-IKa)jM0K~|!zM`Q@@#}Ysb$)(Pn2hMNs}yX{jKi{6 zLFX1fR2??v^Y3rXDf26lS93@@V1@7W6(8SE(>#3gg$fouGqrj!E>R&@N(^F@lPXU8 zij1?}o4UWmJwkEePqxtu+V?;)bGg~o!PXmF?dz1lX%!9nO4lX)8yi-}dU76pr&~z# z^-jUZtSJ%ez6K=#Uq+@857Yq7LdG;;sq0lu79?(fo2r-vmV*q3gg#w7!m}94|mwn}L?!Wnj_du>iQ;es-JXsE8T{dsfI2y1lQYonQ+7(&) zj&)xjDxFKKZdcPc>wOXg(qS3i=ONyGxE8;WucBcF6CfdL30)}IaTUGiGa)9!SRJ+Q zBpd~yU=u^j>ScHJ`UT0}DL$|6$k-cxO*W<|aIGP;X`6+*E4s@2e2yj*9CdKw?55D>4h!2vY6Ri%5%-Rpa1}-0N1&G zX^ZkLx$^CiV(XzdRL5ejtxp%V6^|A$oghHBc_m{>8hMWsD;@uIUCa;Iz$LqQU$=Bq zzqF-zHI^!Y*YVzymtT!JV0@dHZNN- z0*FAHzwtC5yyax)jX=N9%IA-Oz=sVRBXQDdnmSxclwyr2=2eDduDXbX*CB8}F&GS3 z762aiNeXLb+t&pi*Sp63vd+tl=OZa6Jhyqzr;nX=WaGCVUICyeQn3b45b$qSqn7r^ zt7$rUD}zfOY!4s=5FX)XSz`rIbc$cyf+??6 z`k!_4Q$~1xm`ER8ItPRmSn_+@xT-ofczUDgYe&YK%_#fXnGWdzYpIungHM-&qD)0e zP$nMC!4`?7?Y>dj@5-Vd^(t_fIZI{7QH;*WxyMmwRgyNrn=aLLiA%B#+-^Gfewd6< zuBx3A1F_nnTZSw(RS$3;?tj)6_>({ZjsyDl3%)jyUlea>-D%<HVAHvj*IpwO|rm>NAlwe#5Wc(?sqZ*QfYi+}YwIF~)7qdTl@I92BH3VNa8G_mGE4qPNjXx-O)|qr5z|?5u{>T6Bb?}1YM-VWu`ZY{-MG2?t*`7d^RLjh z+BR?>_RbvGj>ac_q`Wn`t^D1J-MR1))pGs9gW?cHnZyK#SxrMwP*+BuWmoe=9YGbs zEh(O3pTTxr*qr@Um9xAxv}C_$o1=AcY`KJLQsqTfSjdUBBq`7a*0Z111TmMe-hMnB zkw#I_%HvW>8YlO@s!kPe;{#Z*fRPa(3sN)q<7nSLe*Wj3=#k_!=3a-#)VO&|3C|@Z z$>@ax!Rgn`dbaUQ7`PvSJk(a=^N(Q`PvrsyaliJhoU{ox^PZ@-OM45Zg=J&pS3Cw6D)?_P{w!5E&Qj!UT#s~- zc6XwRz(^k{M;(G?BAm>SNpRqMgr#7tccOMh$mv|L>3s=1+`WqvWEL^S-MfNh=yxw9 zj((_3TWlhwn5!q?7)@`_uW#K_pf}a@E3pC)M!4R6=!2XtrYaO;aiJyoe_T zry&#_n3X7{r|2G~4K<`A%LOEfHyK|g;r2081V(*WH>6|F zrF2L|f4ZjD>@3ppVCOfF(o$$zV|ay=N&1Z!JNICa2Vu{RSs+6y1{=|s!Cls8_7k}%u8 zrSkU=!vB-MZ=w2K`IQA0`PC}>CXj)|PvZgx6K9p=wHKNJ8#< z`=8#+PK-k`UCSk89H>7&L(gh6prKcg#E|Qx=pT~8pHtQ~9WJ$rSbl1I7OIXJiO57U zg|G47(!9lf7$5fjC|cH~adby1dr;8NG>hSe==>nKl=LR9lQ1s#E0sd(%^|CGu9uf+ckmdL+e`p@RG%)hs-{l{%@!VP`7FFFYE zA$2qc!08_87XVRH8~1#UA24KHU)`KmX9Y=J>ik3Rl3gfoV*cwoKp$m6m*qB7Ukd$` zS3T|wSiNvQ5ZIYvZgA$#&aK6N;z$0s>bk|gsF#zh*|2Rb_MQ4_THH8o&kkp@+gKleUZ5cgw>`o7t zuMZjGD&Hv;tWY)rcIC2Gg{#3C=KA_VKKXtWzIYP!g(D{vCle8OD#$;}lBrw@4<%X_ z@^_gH;Rp3*95eFKap+)P+-Tk0?=S#i6)E}5n?Rx4)Yz=N6P7sYxEa8Gb3NA48lz{_ zLw~Ee4)dAuEHCSi{YqI@6BJxQ*51J~ldx{=^yV7?d(xvXa=D@dwU?Ot7O z2j$G1(jNGWrzGir_mo5fabXy^34{j|c)}mfF%M{5RnJPH<6#*FS@}0!LX5q!PMv~&47XjBN+}l@Xkc7jBAtUZW%OdtfL@PeS3+hf! zt^>Lga2Q0#)p(exXXyMJ^h#LjkH+opiPO54uG}I=H$&>ES#j#7} zR^appaFIiFWuZ*w{;%TK#R*cqMIznDSRdJSe5)>eUf^F~$!@??T^Mt%Q(DtgGr9Q^ zp+IzyAzQX*)uJnC;rUuft*V90<yfqwdg*z5%Eg2Ie4}ZZnzOuLwk5Iq(r>~Se^KPImOy z8i_B1ME&IvJ5PE!)dpK+|1f4Rcz;Pwxg1B+YKuL=eVeW5D%6F$q|U-nuD3*C&V@5V zu`q;t3aIr3Fn?B91s)MpSFxu)`~mp-<7cjZa1=6y$CNM?%XYQ^e3zj5zI)%>0~4pk zMY3q^^S!JL z(~nsQ@ZmD$T-9!!>jLIwm4E9>|Ks}1-mb8`9{eP1@g48&5{^nUQ9pu;5dv%NAq48v zrc#dHl)l15GBWXfQkp>i(HAOD39eoQlx6w#<>YqkfW=V5AG^cMrj^gfNzbm|P*NtW zn-JKTXUak)2&!;?xj>(82KtFeK6H6yT4pVJ%vt)BqISG54(+@!F zU)aOmMYMfD5%{2<3SWh=LaQ>620Dod!;@M!X#m;W=cMo@^rRj&EBV zdjb#7h|af^3Q@OXLc|f_yw>X|=*rqsf5`1PY*L7r2kOdYAuqp->howdmX&nN6EJRT^nMAL#b9TDs^@qLIb%rdrZb z2cG)SprMK%z6c-#-N#Jihsg_Ran~5Ew-*{8FkwNGUjE=sENL4^_kp$%=Nd)Hm%6okJuh|Op%oCbqAYDsH>b=_y;ME{TPq% zr*F19ebxQK>6En+YOSuuN#j~#VmSd+&0vz2Hb*2zyHXIPwo#)?DH`>l7@{wZkBcZ} zaOB)HW{vxtOBz$XZU7jgc>Ol%5Agx}oG#ICU9>^K(N+6-${@}R?5ApfnQ{Bgb z(8lFqRqgq&_1#8^t-M2vdU~P0oRo<3x)~>oJaZ_std&}LdTjS7A#rh)rjS4q;8SrFBOK*wFb}jJ9Y?!E54K-_2e_b zKB`O;9AvBgKB7cwO5oQ91YUrA$C0a}Nu~=aMOPl>5m0Qb;^;}a=v<-B%NkRS8z>~t z1u^W+RJ(a-q|BBGPKZIk&>+>Ly_Gk9i4Oh6Q@`=Op7-x-z!pb|j8L7bPR#1bC z`q=kygm54$!h1)*`QGP_{ns%hY#}sJ#TF5&W~3{dE_G%$&9tkRMVH@7K=WG&3R__v3C2D8)FD2M!W4PkE zHIBL}B1ZFo$@1$uv9h04;mD;PtVF(jPEYmbYYGZ8TkfbOUGku@87neO#aMym=|H{a zad`ui=;4SMO|lMC9Hv_!wGHjMvZ8US{Y`W4rw``NiHX+euEIhuP+(kKE}i%Cj|cXA=&d`t6f;YRM*a z#PV&;SyzFEDppl(EC6X2xpkX+SZi+4CK7jrpEK;dChB~y{3(3_!D0ZAm1r^;h?T?u zD&G~xU^eSGy{pcu#Z+1}2K0;dJFNCMDP|OwoS^Jmpw=$ff)0Q;8w64dhS;Gw`|+8! z7J+ZtQk{PtZ3Y;)5BKB>yl?n?{b)>}#%XcWRhdHpUm>$@((?p`FRw7Ej}uShmLX#5 zXVPC+!jc!eEDmZrMmmphaShFJkasUy#W9{JvahTYX>=bXCb=qOte9s&Zn~q>T%f6u zSVlf-y97t)68!j*dgqdaR60755s-|bEG*nTwm9*7R^lwT_E?QOfQWXrw~oW^y#Olp zi;4@QiiKlnMq+e?$+Pe$-}a}EG4Ko#T22pcu4+KeOhhjJfbwl1Jv$S!&0@$8dh=zpjNgXk}F;`-hBi1bd9|auS_DlF2<-)H@ug7#O5|!p?{K>IAaUUN|DXxt&Ir&f| z+8W5u9I3S(o40)QLapxd^|MStipGHS5fFnGRxcPXI@>B8|_NzUpFPf5t69LIk0tbs+zHx=WSACy^J zy&idvmf9RSaWKld=Md;@$tBE!ulVJXx?xo*)h{Y_mss+4M^NzQbQk~Hxfnks=F2^( zKo0eDrZlrok!KJ)BJ%X&bCHmHyHtx}>|MeG^}qHLH0(@N%DU<}MYj2NeO)IJQ9+f? zl|m<4%hJ^LdhEbVa5J2qdJ7g8;^K@dWMnKLxHoI7;DlC0S zFTTL<&UD_4c$;jjqrXubVIb6oK)SP-_dc)4xbm{&hQTMBMEs{*lOtJb^6=Q{s$#i{ zuC68$>f8sE_(fAGsZHW-v|QST*lY+P7^+#B7)Pu}D72_$YvY@#f}=LDs07l5W`8m} zZatPYhwi9mz+TN*iHVBK{PAcLfH)aMWB7TSDkd&{ z)fcHmhU2%b^IR^Ka)0eKd5k{Iu?au5&1j)*OxYF#2pqs?A2G=O# znI=&bY#<*`7)VJ@yxrzbyEm>>X(yh3J_|! zhtrSOLqk3andT+GGuR+~KzX4lsjw$Z;X9ac{^(7L&{jdxR+O}og_yJ14!YdyO^W+I zSFl~D;c+FXdZg3Pq1a|Ps0?V`v41HFRH^W0gW+4SO=yANbwr(9lCUh*L|5JKRdlv{ zS@9%PMNsVBTG@qDL=B1Q10luApnXbDJ)uWbVnndK^STQsn2q<2flp*Uwc@fDV_xFY z1MMu$^FU9yppGi`N{*sB(m?Qt#F<9jXPpu3o2VGo4_pSYDN!WU{Zg-O@n-$BkhwS` zMnWxE*wO^{hcZ|Zb@?>K+DqA1)4gV+sTw7Hy`iek{AJGg7e0nG^>M zxS(}e#3YGS7=27u#ef+>M(=py9!+(fV7Ve%gPpl1lcipcB>!L@v($>y4Y#GtoFubD z1(|4_<_)|VS?mJ32eMVZS1TE}23?>4w0FYCuT>j64>>N1!Xp~Bm2DE-ef&wY3WKc$ zyRHH(HLym|ZVpE)?9mG_1jsNA67SP=Hel(h0QIv4-nVrk|6(*TGnG^#EWN3^Bs?-Q z&?R9;iIkNtQJeMcYrIeLe3-^#)Y}L9`GCTEnhd2&Xbx;%{R;@uI zduHqcp9#qL4rX&Fs`c!TG0uUOpu$NOxlg(gE1#WMhZmWW;-5GGb~Eo$co{$ z#8j*f2O@lAYJAiyC~&$O~fwK zZOm16MiCU$1FP!2XS?a$odSGEr}ObYuNY^9~H^H^sz z#z5re+u9A0S;BPcH4(Fs81H1uAC#t2Ra}j3F3}kt!MtdE&UlS#DU}mOa)AS=GKw) z)k$h5A~kXpu3}u(@l7mSC!yy*HvGa+H7I6t*PxfzRJBEix|59dSNVwFcuX%NqPL6V zU#rctEOMsbiRk3B@#o2njxjkUve`tlpz?W>0U0to>p%StB+%Q`3CLw#eg@3S)uf!A zYM4=TrW3?)Ftuc;w=MgX0dW@3YVlU)RHNK*&?W~-iSD;*%D6r$9yKuhdEB8)**>Gb z2oR{ZMNxte?d z%|4TDNwsU5%7ccGAGfqL6ZIM@!Hgx_Q$V!e;k)vW-a5TiyH4;W$Kim~s~iy|?@EKjq>$klDv zp;F}k-^={<$9;d{!``m)#2=Lz^Xh`9Bwf65I&StOP*L(l zK>LZx_Fc$w!r>Zn1oN8dwLOvJ;?)Q(*s-rEdq3u1!fpKd;{Qr2X*JOoF4t_B3uVfC z<+SsSX!Tn%nxTO{D)O8HVacejl;ymYjb>MSEs)Rs^8DUQypImHW*rrqX1`8`(Tmdv z7s~7OAQxKEx}Edxw8eZ_4;|J^gY#}bi4 z!=u>|zbWv`o9qjKysXQ36kVSc1S5;L=Is1gv!RYHk8|l!KP~QKp-hxJ8x+0y;pKRoqk|MGTc03pjoKn)qN?Yy$8%LomEDfwiA8@J~N`*H%a$8 zfxgUd*}no<67~sTUN3ecMwSRTe&ezA97qda%Ixk`ulW+vwKk?VGk(+MMPdQXWg9E) zW17bX%)&Ey?Bi#JTl)H)>RSROs+i2V$osO8JKMe0CDh)bA86ypXzdd#WP?9Sd&5a} zuTRt;BIe%X5OMqfiOge4BZE3FvC&YE<>|T{3y1j|TbB{avI?;5C{Y415%!2m*4(D! z9V)fMygIMnjt#^&3yxT<9UuqOQMd74T@r67{tO@@H-cWuvEv#53o0kf z^c=8w7-2r(Mw93UwI6t-4x)hvctt8erEL`;(K|GtNI(lij33N_z?x7!M|Rx)*%8b- z0~Tx9pFgXcw;pxYOt+YM?7C6=wyxhO zuf&1|g(&HaN((Ma|52|LzT1n)XMXUsblY(|+Gn`i^s$T&032IA+6 zYw&@5k`CLS0vGk!mTKil;;&?vP>+XUu(6USef|x0-_45K*t%`&1}g}B6Fp<(H;Tvm zCyibHTyg(AMO;GZZ(Zo$bKm{SstV_H_bd0LzUC_3!oQ>N?LS46{PWZNuWqHLH4a#; zJLX4FxN>^4TJxy_#x4{&qg{%t$+H7tFbXziSv^7rJX!Z~8X_KW59OCHmD_6#^_Y;G z&j~YX{tq6;p1zL@#@1rI73OakT`7A=f}ny`rNC;~RyG&QIz0q}6NDj(S-m^TJB&2f z7fXgVjqF`D0VPHt=I4taJSZ3-zTMo&Wy5%xc!P+0oOf%eCoBqgK74l#ihGgz_9C(t z?ycn2AT{f|B;kuMYcvpF(QCR-`Vv=O;u3Z*hwZe|3mT3Af9r~tt-rV|R^}3TCAO@2 zG<`{%smS_`SN2Z=obOvL=5jj=6m{kAP#!2yihY2yE$AjmPw$H1JJPBL#5s{bquK8J zeSEna>uu=UywqTOwlLhKn3m5q zH^OTWx}-eTJWVr}dq6=TAJAfD`bq_X0_^J*(R~d<;F)JAkHvG_O_jIp=dgdU89=I2 zJPC#<8kO^X5C$rkoMlW7{xpp~z*KoA{b>C9y>V-ox*2@fY!ur-aP4=wyfB$&U%NBw z_qxjV?R_hoYoHw#7Ph5K>4!e)+CfXkbXiFQF4ZYM2V{5igw21vx%(`4104U_=EI@~ z&htXZ0?ZZ>Bvrv&I)&n-X3F99QQ2JG<3;1WDCMx0Lp5FYJ>eJ_*za9#5nDMIEBi6I z+pM_ld2$)GU9;pZM=#`;UE?g@?#2=+sdfo*S4-=H2r>{~=SO^duLg;4w=46%O{sD# zF;G%XzplZglcXad-?&m#M*+e|0~7RX>;Cu)z#G08Y$W+dl;poqqt$p6`IX0jhD2G8 z@;A_n_*uq!#Elev{uD4R~<%;BRT>`H7g zfs{GG6WaHsO4Gh}An<^d+X~~?;v?`o{)R~2he>*mIpMXc85CPiQ96?(1uJV- z?_|Zpof*Z0s!E+_@e{U+ys6uBmri(xuCtGql`%(lCXClsJ;q$bv6Gp|O{&WfD?_t< z0fl7{(Ja9xDjdnx2uP^&SUlb8t8=G4yRV8G(iXT zt()=2bh$~7Uy=a61Ry$i!f1SM?hF6M`=2*J*)crxoWU{V4)jGy+kWnbkxkTw`uUTf z?Dt01ya}uz1^8L<#M7Ve9{FY0IlHw-u|)E!wVJzBBy>T<7_6>C;orW4Ov&2`oBi+L znrYa-(8nBS5EI!t-wT6v&gC=%LM{)@%WKvEqcDi;zQdiLefoMD7fsW7o zz%!Z`cnX%apIRY0{81J}Hm~$fBj6zvk7a0hI84{tKJy#z-Z|E8`Kqp#UCdO)iijgy zhIqqb&Ujc^54xo`SS#fs14n+)byLFy%P?|v^5HR>MQ`4Z#Kak^Gg~RqQ)5Iq zpn)rETtpty$?I_QD=K7m-EX{PI_b#fTLm6kXkOO+a2fZF@mMWabqamGN2>}oTgx2| zp&ik4=K2NP`8b}4VCLh38MB<3=r(N1*D>7Sp(3RHkCjrA*P6W8bmV;CEuj+an9O;m z%eL5k2G@QJR{Z-B4g<;JP=1-{Qh3>q3*326XX5^tt}YkU(m1nw^ztu^kH6nQ|L2`= z7yPFl{zkCox_tQpn)C2z*d1QuX%c<)9W8#C&^u#rEZD`jH$8eD**bVBt{ra`o(iul z%JDhtm9%-7lXxbZ=A_leyuiR?y&*}$yZYx`OOkxIe~b@gs)dd;g^FzjEknUmDc?JHaGHd>LJijsALS9Yj6o%kQ2*y=+TE#%CJFI~BOI!0 zxA}ce5h2=*tm1d zEq=az)AlFR!vhLN{=VuSt9+NJZ#@c6A{9TZ5^T>-*?6;sx#UH%^L&UCl(x-1?-njq zY*9N(VjRBTvm2aHn4;IGq;Kq|rNmW8*Ahzz-capK^wlVFbyF@pQIg49c!~Mc_fs@H z+)7ajIW^Wv;AkL>c?J-$Q7lHlkCwWP4U@QY{WWFAA`rNE57wiv|3ZQOw|5b4jD68r z`N;FdjjA%)K)sr$K{TL3mvsM(uC_Pd`)+MswWN$favSMuvDRm1{fdAcYpZ43noaGv ztC-JI%@dW_4R6+~d$@tfo`E#Zn3CK3BBcP5QOP84Y^kosP*83 zS1CnQ;WN3s9X8n}bzkrqtTyV{0B_*p-E~}PS@g|0dK~%oLtPF>7vCI1bmZEqM@qz> zPfj4|Aw&@anccEJ+4>usG_C6=HKKOD&p&d!*KGG!dLan)AKFXGm8=ffXxXA_tQ2T> z(07y7K`yixfs}|W1|S9M8Ipy#Px#Agrj26hMth~P zObu!orTn@{)-|Ztw7mF?&6PSn%g*aDiw4%mmJicx6ZiHuC}NVRYe0#>n>s-e?p5-4 zw&lVG`YHH;mfQ&`Cc}7}$g%m)sn{&~f>7^wP~x6?Z=fG%Z8TuQ2r5&r6`0+Jh~>G% zIQ3xae|FpM1oiP!a&A(mge}9w+VHoN?1<>thy2)dNf+aA0#2$P6wk14@0Nm`Hha8- zk1SMksm^JnXsXS^Lzz77YNO+L;>+gcr0Zq2k1$j1Dkg&3N^z;9a(#o^PmujTp?xyEDO2wq^=ehjAH}=cK_9yw%19tTY z7^t9?o zxs>?w(JA@|dyv2v$>h0rI}g4GKbH0BY&FDb4qgrXNWy?w0|Jj{MwiiN1cb`W{@UU;EFk&ndS zSb1m=C9t^DK!^Sz&;x}!VjxBgJCdf5Lu0Vd6K(aezwwG3F(kIoEEf}q zi9=nZv_(Z@4^cD$u1;bP$RngXBZPv2<_ofy>E?x;Cye z%AZE6{N1flK;NUQtf;h^h2>G8Eb4z^2=__d$Y2w|@J91W4 zE}b?i2t3qUob^Wq?{*WS8Be&`UFj{Z4GSq%{l93yH1LSCM#Z6~MG^q66u!OPvvxi7 z(TSoU&L(}usUDhO(I^i6y2{kN*m*0quXJm=(k6U$Suz2q)R>KRRMz!%yyoaqGZfRC z8chuUDMzV8boKf|Y6bj(J=!KZsCSx`$6(9;Wl?Itl_#Wuw4Hoc#bS^2<1N-pW`TtU z7pw!{mcZy{UCiR)qKxt&e}!=F{`4qSvYg~M{xbmdvpGVCnx8@2Bfs2S&zmkddjuhs z`a-`!O2A(<*_}cnMYX4T16pmzCRb)N*fz{nqCMo_q=Pq)2fvEmxDPm)XP=l|+p4Jn z$Qs6WeMFIe_O>7kUAFku=Y*6QI#;%9eo#zdcVBj)e!ca`0anKW=uB~1NC@9eU9lz3 zB%7s3ibw*^oqBZDMR24`h~gkOK^21Y_lm^W*NbDz(XGBbdD0Cm6gptJclid{|Vj6W~rTg#{ z$N}@oF`JCj^LTL}&OhFGged+nF2~N1vfsI9yJKTKP#lI3=v0X(k{CdM|%x|C+)J34RAlYe2Y7B|^;Zy8sP2M?ghzi=vO zT6C@n!(W@5SO`lBG{0~uq$(meg(%cQ?G9jeKH1*nAvjwR9-c#u2!+1=&IwvSk8;jTqy(1I| zZ1t1TI6-4Z*N$h@lGH*ulve7M^S$Mr-{MY>hX>H1J5*m^#p(X?{l*hs`dsi28~(c6 zzxPMjriHhCj~!n`*4Ul0poE?rN`LGk$a(@IhR=g_5fDTMla*QoE~o(V{zHT^&mz7O zXxvNZ#ZC&FblwN=f)$iYRFKl_!A-~T8R9t#hMcb}bT*?FO2ExBniJiO%98Ch-e7B)&>%LSf1qO2TIF10}@ZFc}gqR9t7S%Y7Zedo$f zj#;Yt)w!<3jYq3mPXT3yPgB&8ENSmdG4ZlnQR8)7L|aZjTPi>rrjiW|fb>52B9*pO zM(9MHD%32BW~E=FTt>i(e3Y+#KfXV7-1$izl@nDyqmD$DHa$I4$z)Qc04KuoRpzIF zk0S15qmCgXEbHc^#`VdphkQ>`6$ub|ZI#T1vA4xtI+p4&=Nj20^Up2nDJ9>D@(7>h zUJqip$LBVmb{43H&8yOCJHj$-v`k=0QX|C*;Uh<65ahJdvbAq3*cRD= zCQ6h)IOo=EpR@7nkfDu$h80moEM@^BE^ImpzVpb%1zTm&kN()vq=!pdjV?;0(y<@y z=s)IMj9N5*hx$;=%U7s}1SSQcUr*Lae1lO72v|wTcQe>Bl|T*;lfnrZWzHON2x$?& zJRpZ&7cq6blq{+goYxlmU3})tte29gG@5C-C{&LET~+l(AO-i)`5#k7;CO-eA2C;a zf7*6em)h`v%=#0^kFb1-f1Ztgyexj9E79h3r!p%sCf+zJp@ z=Pwp@zf;r2p1{min8YWC+*HHi->-uXUJD9YY|o2rnH1m&`^a%-4e(BhD2cqv{+c>0 zJc5}9o@M~||0Z;Vy{mPeEAW8VU+B|pVD2<$cFL9ch?R`MIEpEk9x%>X)E7F!9pKHA zt7-wANP|8tThzC0gAY~FuB$2MQ&4!sJbm@&+|q@qsl@7z0dZlm{Nf=Ya#VwY4|EOm zy!iQq#DdUPW_=S7wV118fF4J;s`g2B%}{TXc!RlrdDVe0-?DI;pBN=wOiss$t*t`5 z^`e17fULb-zLztBvZELg%9hj~g(BMe3+jb!=vP~5NZUgqok&W4s2Nh{9X%!qXoEll zhS?Z!%bN7tS09j{*PA&UCbSvyx1!l>>LH#atEP}x@tUm^cc-XWoIU5eo_&T5d`ji3 z0*(?4?&la*ND}BE?nE;3jaeNx{}5A@_g&_j4Od#}L$G~UjjxIS_)W2{e`|5po1 z*7$`YWS~M%xQ0}fPuTsGBmVZnn|!Xa^BM}7X=z=>1-Hw)7?~cRc-JZI%##P)78|SjcI$;euU8BjlHKJX)ZPepP z^X9u|zV@@y&0}*ne2bWO0({2^p%(~)hDaC*$YEwhtK$n$^s^3AUr10%jY0*+Bcy)V z+_hIt^NW1D>2h?3H-qo&^FEFE4+8ujvqU&6Ao|##i8LS^Q=)pkf|%hQG`UxA-i39# zyT`ImF93rH&mZ_GczXTKZSu)v{(;0;l9ZJOiWM>uK#?Jfc0$3UJ7c~cTF)72JA}>i zTZdS0a0XddPpgZZh`8m!sD#Kct2N9vE(lkWy!X#%w2+b9v|WrqyzOQwS8}TKimqIt zO;|xjli+fdw6z-LyJBfLKLh_X3kECGQWgB|lX;w1WPqwg6>NpqC}y(UjI>T3WGJPS zj)dy~KtK-D4e2bN(5RwyQbKSNls4N63C%~LvkNx$7fUd8?)GusDyTT#0HI|k3GIkF zcTh4`fLH}_kd2|ZA*{xUR_N$|-ShT-68kXw>#K`G_8E2cGKiVE?FlHc<{88q7d#7P zmcQm#W&6G&5JR|Ra_*jd$YXq7?ytVg0hl8NOlDz9P8hGfFs*0763iStTWjFyXJ*Q7wQ-s;Sgiq2p z-4d2XFR)eSV-(LPizlPL6;}xrSY^rnxANXPsI6%28>L=KX$!@I7AI)26sN_L5GcV3 zQk(#R;!vzY(O`i9L5mYygF{=~-K|ihXj`;sZ_YdS&OPVOH}ARc%x5#-{%b$kJ2QK) zwf5TkdDc&SCX=jR6Lxaw!!XYn-Q$}l{lZn+`Hb037Oa|tHf>(S&JEO5mv`{x^rwIn zmSR~ap2lfMNmPjosQ9ZBbw+Q;NsO3?jRSTUSLyl$mQ*!45L+9xw`Yajr&E%?WUl7B zg7**hGk{Ji%H=%}!i4`Lg?y{AHG9w&t)x%#8rKuvm@+7zFTiBiX^5Tkfq`TS5t%vNM0wjqxi4JHn}yT-Thv^V zyo%FDdLGvFctP>*c~{W1C*poWk6=&zAPmK!T;b{(i=){v{MY&B-$d6Kt21I8ns*88 z5sO2gKHKFs>U+5vpH|4NxT*GD&^dFDMzui}9;#^!G6WG)#OP_hjPS{FGL8buc-l#FK+`R+q5AHVYIP9Wq$z)jyoP}>mSgvapKI&F@J!3maYjbg)KFm^FAtN0xNC^$c=rf=uq z(K*L~%WfZXo3m_lK)ZbwPf-`|lpAO_u2}}$8&Co7dEPde=%9VK4^;%S%U}1tJZCJE z=-*eX3!(7S`7Q_)byzNY*q(z4?FlW{ooVIdsN%@?acU_!neGX~WoW#c|HU$N`X?Bi ze^}S)lJ6&Mog|02*IVIwfGFMy4PU5bMo}_UxaisIRQiw^a`HjY2|2D5fJdd9d2!}| z4T{-;c$@fw+JFz-c+X6nhJgBKjBdpwp1igdDCG-^Go9PdPV`52fBl&1PzuXSHW8H` z6f5~bv(-HkH8xV?+IIC)C#-{pY%3;yzcMk*&IK%nhhflYbT{&sW47+@-a_$Eb=HGt zB~$PnrThxyLJEpps0L1n^=!&94Z|HiWB>fCzcrYP%bKpkqiCcq z7)y%ni%KNF5AJQ0Bu}^^;ugXqW=-zvxD~O)?3zsZ!bEZ-O}#yX$EBgS**OxjQ-lmX z)rdAjY#JRE6sX}lo}WE}BZ4mlJ1Q8tb$LY}w;#}$6jf9Va1S9W1qb#tX2rS~eK^io zcF9Q<&^`CyFw*(ZDBncOS+gQ9z{I3*+n{05D1CJrU}yB~n30ZJ#DHDl`&7esd`zD@ zkPA`4c*&->Ww`fB+8nn9a{^$^z2h5BML2B;np4UyRbbGU8bH^0&7SXW?@TNO#gY80 z^NOOTmeB*wcd_QoN~J}qb5vCqIpZ1pDxjA&YCADX1sA12)V`3LoQCtQN1G-fK$rs! z@XM7}p~R)9ATQDyHz-M(p3!0Y;NK?8S7u%8AqYd(4obaT3XXpC0Edbd)8)01ZiBS-WZUFG|Gdo0bE0kOx5;p$^g#s-<)cKWCj z7X*rc>8>1UwR_uo-MfM1ob;^~*R-!q@$NCW=W8&0JtCv*C411F1q&`BJB^z>IF*!1 z*KJR(H5J^ijn5LJkAy(F>&d2tXJxa_{M$iEa}LZ!6>LkQO;xp^QfkTd)$ctP+QpUq zw>YgANOP*=4kclbb#dqVx4XaEUT!%j9C`7Lwz7H3IqFUtdR-k1EK1Kb8K-5iy1IJG z$#k_odxx4TY_PT!3M*n_{8Ce>T^rB+Zm8S#h0nw@7S`1M_BPZE4>(pXsJ1cq!@})+ zu!_lDe|R-u6ngz6Rcd<=%3(aOc5vi$;ZM)|{;lk2E(8VPNrMf2V)Ze6<9*lMu(Y0cHh? z2()HuP1aggZx?U~5Jev_M74lEypzfOn;B0=5i&HRrkQqp>QhEi*|Es*l;%j;za(L0 zy8$`i@ZahQ-C>aFbZFRC+WM_jEqggmGn{2aVEquJdp2gt=E63`D8(lv_T)kJ7R~I~ z4xGyL%O|GSpWXNLa#y9|Y;S1oha2B^vK1CCEU;#yjc2i!vEi0oG6I89RY~L3`%k~y z4u;S;$hktUSEM?qBya8<+wC)AYlWj6<>%`ycR?26ULx%J+(0J;?*34! z-}5aB*A;Jn&lH_HU+M(iK6ATj=j~@^)T79fzCxYDIJlRf%^P}^?{~(G$zY!v&EBu* zR+n_^BRO4gY;UwPMws1C5PjR*B@nIi&iI>^RUwX?+cd7^s%fZ#DMy|(dsFzCRaexq z2dC=$pe#lY3fxZK4Cf!G>}j+PCO(H)Nn&H04s8ySe@+6alwZ#u8|t~TzH;dzT6X+% z_#fbH_vo;ne@O~j$>e|e{=A&AZ2XWT`&{ha1aa63Wr@K+$LsAXS8Uh?c}T#+;OE}L@1MrA#={x}d!T5>zaTl+vaTHg@BdKtPTP%{WvIYUSg5g!&M`DE90(I zbGG-n;r*1;(R>|!^~CwnT1G{%n9NV!MiIvW<8`ps*=;2iU$TI37p79>vdIf!7wHJ| zmzCHNpAJ=r5J~>+ymt0Amn!w_XQb^oo)b~ccZq^?Ky8@LvXJp>u#E83>)*;|u@}*J zB7lCdw$h2wQk;`1vwnPG&HWrMI8ZC;6X7}?Jf`B_80rsM$?~EyI>~=@xF{&^F*wLrSKORGnIEYQxU%b{=G|*S?3_tQmgB@QALNASL-iR zhMU^DDGuOy*C7b^RHBVu_b>K&hp7j#hrUNrg};3gkMKS2o-*oLEi(ZT)((KZ8L-v3 zQr0e24dML{`*pCp=ry~~H763|P6DErPrA18GGE4$3s5uDyrLVDbGTcB>V%)j8COn* z1MVtuio}vBi=DgM54U_?)mGEroXocA|4Tx+gjdETEyxfe$>2G~4&P%ME7A`qEfWDTzY`!fzUye1W}&gU*k{z<}2(*RJRB(BRmUs1$Cw*+N6^1I6tQaP-)~@LmLl?0crK)o zPMlck80}QYveCPJ?vfdD(L^pa-qZxj(Rl<%@hQUIQpxvuayC1g<4~o0pJ%*^f4Gm3 zL=7K;c6#}iDCyVnl0<*bbc8gA>(QQL(eX_H&cEjCx(NH4X0;>m&;bT4ub&M-L+;N) zZD1)X+}X`cu{;EL4t`)@jjwdQFv4g2fdb2d#fQo&YSeFCF-n7~q)2DH9o7~!Zv ztIw?fcHTUS>7mV?WA}L0Z6!6kr_I^~4JqTWdA4t4eM<9@yCydWy-69%rab^Sj(|VJ z)eU6WEj!wv2>$rn*;*cuM9o;rnx|`~R#S}Kxm^zRx>6m;qezZy4#KDxyUPi=06}$= z2i+Yl9kL0V*xe1t-;O93sO;u))|pU(r{X&!!6G8zP_}0|uDD#+NsM7E4wMjM^I^k% z8;pMn6s-hmTbZq$*u5~ZN+1RIrS#S3=ET0?SABsuNg)y3AlSz$vm z)_#B(SFd_*LE-PosfxHNV^v)#q@0!Vi{nyvQBaIO-i1p07k)UG^XSP56o=bXXrTv%*i;zH&v4~zjP zx1h7&dICoIM+MXETu(=kQV@}jTri&N^!)!%xA`4Y|0?;^U0fMHpM;s!7OGtFf`)22 zB1KLBbg{1zM6107EmcL35rHh#@QL6Vxqw&-T9G0XtwZrowVL^+D+xRN@oOh;Ef2@!ZqGP%ozJl3iDjI2gvr`4Fnm@}s zx5tS~zzm0~ASMuW287g3jFSAOeGzvAUKw?P zcSrCQ;~~Q)WSK%vAIClo(Ih*_3Dkd++Bu51y6Q4Z(H6G};{Hj&#ha~mH83HQC3>9D zej^X&pyClL84cgsi*~V1TJdiBG5K^$JDdMSfZOkQ2)c+AnAGxnrUYm@K!%RUkN?MCc4VR!Z+?U_}i%|J7&b~mgL$ndNPSul*RoiV;X7^&?1eJ96 zRavLH+noknh-?G>hE36lCMs9=n>W9@GBaL0dAn zJjVYhdP&+CClKogL-;-Sc+hA7wA(z~Pfz^FED+I;#`Whp);Cxrt}dv1$z^z$% zjHXzj;*{zU@fa}M910!4FHLTTD_XJ;qK5qRo|8C)c&;-$1$^g`~y_6>1ms)>-+M7`qm2Lw)>GY(RKTdq6z1lV+q4GFk)5>pszPEkJjVRYcEw zh05$X`6#As3x>GpCks(>>nod)sife;$3rM)3W?NmhrILE4{){a;h+Pao|`uEpMDD> zH)46oizROc+#cIB{Y#S6HGSigQDD1fQiE0;GGscxL+7W&jtJ@H`D$6^jtm}naw;wN zr_^bv$@OQy@qFV>+A;rjazjDVWL#C$>#h!%jbYAiXBg#QlGVg&^*>6pMp){}d#;+5 z{-o@4)pE*|x|^;$fg?zeM|tRk$XC&urd->2%|HFNjz_kT&AStBc<-F+nqj^5=*guy z{2W3vI{s%EbU6lnE*=@DSsc$C8{Yp=g{QChdLw;A){&_8$Dt1frBgG^gk zyy`)f&z(x5y`ue>QF#|438CbQ;i1_KOQ@zmg}M-2MEnn}>sQAlq*-$Sg1*y!`(92?V90V&_4CL%=v+wG*LFE?or;o4yqmsj|FyzF)q=y=)3= zLM=Pny{^%VU^#918)D}&nzyHR^7G6}sQ>iqvgAG{%R)WeKZoPv230{_7fwo80ri~zh+&6VCNis7ihFbq7#_CUJ(aO(eAMwR9@0Pnhwi%3PY~Aa;p5rj z0`^*Xh!J``7YHF*>ZaP7z_6f| z{+RwjrCeS}e~Ld37ziujSl)U{uK!gWSTA7dSqMvW!SLfZL<$JHgZ`_bgA@`=X17qi zWmI_Oib(ISjb!NCErsvuJwHyW%h-8nA7!{Q#WVdS`2@8F`((|Zh~*mR@3LPzve(EM zu}^>K{C3p|YJaPZDSRn(`5Eg+3QWv4REr0*LKTna6_JhP9Pa9hK!Rw)H{DHGobK&Z zbn;;h4)l1rQkt;PJVRh-CEH{Yn?pC%@ox6hm90XOkyl`Zo7Z0IL}m zkFCk8Uc}NW?6L{iR?w%4&JT6hY}v>+zRAp>tnbIi}3d6WsO{;oCr6`Tc88%cc>@I^LAx@2R*U^)+p$J82ynmjH&az zKOa!m5)Dvk+D+6?UR~z|e{~|)lEYC}@vm&qcdk7H#Zgt)L64|^iuqRNWLI&gaSd6b zgc?>}BEdPK-EZq)UuG(sU;z%PQMj~@%O6=EWkC4J>qUJ!gTC#ubBjY@ICx9ExmL@kc4 zGqy2GZT7|%{NBT*c|Fuw7neHYIC+VRi|nu$0mq^4YFqbyrEV22NUV=dRN(a{iCnpO zYH{deB4`-9ak}WZCzvb592RF(%OwsCf012{6sVp}eH~+iaXFHzWS5Ocgx9QbYCvJq z%q?X$9;kRPHYj@EC+2|fAM8M1Vnxya^L1hLud0ym$A@nGqNzVQ3Omx((E;!GzvWEn zHoO-jRCA<2%Yc=#F|Lg6BP(g>TIR+*s`5F6bQiv=>4Y|~*Gf|GoF%hP-G2K{DjT;R z5;VSjdzm(WVcsFdO_W$IHu`+CO=x+EqzCU3UZ{_Hz3B}#34Z1f*`scVbxg6EO?I5b z5(wiMHf^U&JH2)r6)P1v6UpVGTVpAygv)1=%us2m(01sFE1(;<1kQ4iSvL;jNXBgD zjHab<^5FYuR_Sc?QUu>l%CA-5R9%FsxayO0JK@_24D5 z{Pe|0eylYhaDSX5Gyj`|KgTNx`m2ZKcgu99h6ddF&?@f{+J)uYeDSTC6qDHgq3oHs zz04X4dIpcROs@Ey^dJFG!i0`)rJ&$Wmj3hLnMO99d`utzrzV}6f*v5-32ujr*>pu( z?2zZRpRdNJ`r7TL1apKTC-?WWPRd>>9p!Jf!q%JeovWg+uUx@f--I%T^@Z0meM zhh8F_paNk=p35i7taJ==Wl}B@7CEim#TcS{<2OOGl*+Bz^1r>4u%{h( zqv@8eJHR9jnq0@mQ~v##8Fr`U0c^nM528YZI^#|yKm76A;SH%SL^OhiI&`8LyRON} z>y&>C5Mah>U3iLs>xRSx{w3IL>8f*MC+ml=o2)E}&_oExR z)Q1SKLop^rDH&1iX`Owy#^#|Kz>CPRiFlhytNMo}_MS@lpsnwNM20Z`87?_`*76^O z^c&GRT?N8P5$~e3rY4kK*18doELnrcw}w3GkS{3c2&4y<|CD1pi_40Dn7lB_!ILK4XG2aH6yij4uVabzOeg|x@kLEI2(K4ArHEi zeLN{8@dBgvz)iW< zas*BM44o*!&LSaH;9u-wvFS^x?svi!_3tNXppy=DQ0@pKg++ZRqX!}nS8=xM?yj>Pg7idLPq~pQ|x1$H`77)I+%rbC1%_O3VJ^OBur8HcL2?)xpyRS>0v z^BBGRkJ(W7w17mu37B2iw~ zZraQ%A@bh{+|4S0v}|+rkM@NLozw~Ea;pB?z1#8+c(|PAN^*B}ioj@pU|_w9ytyOt zG~SIZ_Tr-TIZej)PAP#=QSsGKSC6-&C-leeV&EUIYrR{8=?2#KpZ7hTn){G!3DpS? zvzOCpQZaK3{e3wmn5fA9L$$I=&9wnppgS8KnYAKg`)UZil4YdVf{?#%Atlk<_buSg zw=5Bn-vHN96x^#go#&u)h*%BF>xwZ`MH+uwe)4)OP(R*aU3Xq%E5`w0lVamm(%u>N zFqb(KU&i4|-`AFrnBtVOJAA9@X7OR1cFHzOxXBB{za$&tbSr%q_u3ym9&7fDoV-Kf zU!NEf|C`R4c5E7$@ooB+!Oa}}pPW*GxpDHRO>Dmur&G@+GCt44KCB(aSs&a)7NseB1*X4U(3TGn`e?stIz4v?SD(^@=LcKcM}P*k9~~AUvEeZXuIEmtpfg} z)D_?Qc{3?_^ZCpbM4=_FJR=i zoZ^U7M-2XOjOsn|m&Eu;^4z)3qlvc<5&WWnoGPYxu~{;!Oj3@Vh;K@=Z~y5M|BJgf zBojAV((UgDY^J~RK33}z60$`Zv{%RahNeM+g?6k)3zV-WwBjvnGPLN{^=*y52CnY3 zr~~|{W@3uol7fFM`kU3J_(8U)thm3*qKzVGTCmb&weqJUXWogFgU&Z^}rp%%snH`{93+O@M7p+lFy1KeMH&3vDJLY^q-4Q28|cYDr$u!q*^^I z$*KyuqlK53E!q{h+L48sr9XF;;V$5(R||L}bG;A0xnB3|PnwBL*vvvV{(*%5d>#JA zym*rPs{glu*Q4l3*llrL+8^u*{nT#o(6Rb2%QY*7fRa; zy~}qdjxo}8*|x!8i?QCtMDXt*!^6dnv-MS&Y}2!tP2kv>4v*4*Y|6u-AwT}9V@UOK zSI+YH^F!@zaKkH!0ZLsz3-Z1;x5nvSq;Zw3FCaV3AscJ^uA@14;AJeAiZ6gD=z{g> z>X85D3~8y{34C!U)ld8C_F>VsVAtWJ!ndUQ{~Z7GiT~UE@$S{L$Zh4TYOO6$wnd$u zJR*AK#640R91kS~Ko8a?ny^42ePyVff<0o6@lS2{AG=?-9+ZxJBRs0` z#wHkYAslB@OS|kDpqRxM?$6TTkpi8su)|9}YR-X5=!r^AiBtxon)|(aSzCZ*Y=oRe zS9wjW9P+#e6wO|vb+o;Z5@lsQVlVV)Y3=lwJpbrQZ}31$#+9bie`WGZL1g-(G_Zq$ zu8k;J?PFeHUSpVUKCmUN7;c~zUY2Z>tnPeSu>R}v-Y7q1Tc7v(Exm}i=|nf-duev- zi;M?W^s7Rsp~=@5nv0g+*R}U+3f+b-KP<dc5Aa>-1{1s*6KwQwJr zN=H`2uZ}&PO4WK2n>f~=2`(72U~_eVftrUh>2<6E+JtqPA5_l`lq9A=BkSTI!&&q3 zs>A;vr{Bq{oGjfV({rEHvTgILPmU#;42*sJ3BlN?mvO&OMc*m)p_X1Ta^NJSFJfY1-5)5<@nM)ho zL86XOmw*Sf3Y)`(Uh`?Wyw`I^tBg*`pq!O0H$uVzto9-BPs0|vR~Bl{(rGo`&Wy*4Q>)n4d#d9y9G59L8ug#aGlfZqmk)k>^Db;l zPY#{)!_?f>a$J1rbPG&HUGQ|a`<*R1*_WwKh{zt~I0V;RGZQOYB zS8GguU)c1}QakLTH-MbZ9gs{xUa}Oc1c9%#i4 zZskw6H_ZtlNsW^pr9*}JC`<9WK^+veRPiS8$b|ifLV7I^7Mt2tNx5fv39jY&D_z~( z-5;R(T>3j&xf)6Rpd~{`-#Ggi4OGR?9zs`3ThiFY4ZxU;g&Z)v9ie3iPxskKa8kpu z+BQ~NNd?4}NtBG&6yb3f?KkclY2$r*PSQ*!BJPeD3qhH_rn=@)a9pkRBg4&=si%ELV@+9vEdQK* zJ$&Vjr8VSD|A*(=%uGo$gR-D~(4(i}$M- zGq{8hqBcQYE7_pRCNR3VKwYtSecR0k9Hn16$6EJ6|5gnvSsX}#4#{g2c9-3#KQs~o zhZB=F5wuoI$Q@Vi{9_gSPkMOB&_U z6w5K39ij1p#tArR6lV@Y1P3M|dj=?Qq2Ave+N9pq_xMR>hJ#cv`8+Ae!)i2N!5%_$ z0e_92dxl_NR~4p6_6ez#K*Sq`_NV)jPIF+Gm4)v?Hi z_TVtB0icV@hZ) z3=Ed|ZnXlR^Goft%EhR6S$)3kEH-8$Gj139G2_*t;bQ^jUFtMnI60PL58^l(C&m+v zCu;Mkj-$~?lAGv5C}$fw=SOGJ;s|RCKD(RA^(<}3S&0AR2?08-)F02vL3H|_4sA(> zSy$#Yehl8$;4$!T{_v5xWdO&`8$R;=2w`EEdwZ;RySMEJ%Kq1j#)>3-lqhFsfhaxc zG(lU7W;AaAtD5)#YDHLdBnq7TC6W9`K9jwiN zoOpGSmA(m77ai0=F1NeJNyX&8Om!oSb#p;+8!PU4Dlis?i&<>J?Cz|iDhjTC=Qtse zS%-)hlyFln-xtYa{Z|q23cDF)$~4D>TwxK$pnY&z!pwd%je-oqr)Ce!ahY51q@wcX zB}=fCl8tQzZCF&PgBNwRXtjfuvpZ~3Mh%|k7*|88b^nkqnB;1IcqwLd=%kX)c%@_CccML7euIsr8+250_ zik+PJ3m%kO{sfJtNLoc{b1G}mI=V`I!r0i9oRRG8EO?i+IaC;HrRoa}?9cHB$QBmy zu(2tBIS*V!>K$-K(1SZ&#Ai#ijKvGC^)Hwt5=h?)M z@o-5PA~r)f=j;J|$Vk{e^#iDj<9Urv>b=N_bI#1Xfm+LkkofpYn@Vy1aH$hcN@(Gm z43kX2gR;&3`%6k5yZw25$=>07;Q&JhA`?L&X@z~CO)-p}3z{rlATAUirrc|zA~7lY zrbJ=T8`JaqIF~o`Qo2-$)VT`*OzkyDH)&L=WY<<{afKYu7&j)YTG>!JSJ^_7LKI_- zEN~N^oN)|}6V@WYXC-l*?er;8>8ezxB{nKSe3d(sC1GTGYb{fnM_bWS1Gby<^P+<7 zm3L#b?b>C+o9i~@D9YlRp5`sM@Pc`|_;RP;aQIzHIGk}zN=+~bmh|YMfD_&By-BLjtSiaVfp3=ZItMUP$nf! z5MTHtvP3%AF9Dz+lkK?m>_?7%mxq9Gy$8FeLV|I}U=M&^GieOQd0=C7fZiKu%6REW zzn12?jbP)mmmOrhJruA@%{?5G0rsVw7cVQwdK*XkkouYR?3Pp3hEH3eoVBRbuE#I0 z^TiEgt!X2&&tc(3lEzn8CIPkN0WLEzD|Ha0dO z_8V{QANsU^G9*G{-!__CT7npnR5ZrJ9K;eK5}7MiYTXo^Eb7A9(u*1l0{>w62G~*5 z68lT?G6~ClZO~;0o1~FKX`EkWVPWvP>17oX!cmlYD3{GpY_u8i%{#!+d(kXHeW2nE zX(IV>>#I2oStSw*NkfNFp4isYtQ$AW=3LK{zaQFXDbU>sTTtr|?tf4wF|J-1LH|*N z(Q7HuTC&dN<@ZQTBu8z$l8O2;yKL9|)u&flF2Pku!{!?j+ZSor#su{|%F}VMD&Kl( z|7pAxX8H^lOZ$?M8TM%wh*SGn4~n;lTaP_pbHMUbCdO|vI|TLx z(<##8TUs(H0O5kHxE0*dV@W|kj6!{}-*`MYbW?z%I=xpRqJs>42&YoY!2yUb)wX2H zNaZo<@CYz{RW^BzSyH9a5!OOI`6E{3{WdckIs#;DjDr(46zah$d8aYv`q#d^n!$2q zE|1T2g1+~bGzE`Oub$h?6+p&X*+bVDag=cHz|}e*Q%}tCE4T~DOii(CTWJ(E>MdcUdC?J0 zVF-S^>NsHXi@zjaImJSWq9BUln(e3`ZZbDsKB+V4 z)Q?_`$#5Lqc%9nbnTk@bh}{P&Aw^`H7oNLoz7)6Bglf95!t zJ;I6;YBue64gT)bs~9OgINQw0T{UO9!Ttk?qoyI|MhC+4(cDE5k;XqBT8pfS8X9w% zmn=c_ogh1r7?tf^QsZFEl7B;l*}o_&t-1s-S6h_ib zCTmDMVI~|5$7sz#jHdhRLDxnXR9B2VQQ6pJfelYI8%n1)cxRt#D~;B*GK^B2S371# z^5(v=5n^4eDWQi0!Ui9*kS(oqX!&Sm`0)e4gAn52Noq~~zVPjyZKP_-3jq$K?^x1- z-+*L;{$L1sJH3uloIr@BT>Bo4yqg9#(5GhUhvJULvgL``rv6Ea$yLxDts>F1XJZRw zV-4N(zjFcNlN71x@Kje&?j(KoB!1@hg1ZlVm6Z%c?@vq{V&Yb1(Glj|_NIX!*A2>O z04kgX68`w5&3VJ}>@jQcNs-~CVPhwS_;LMtBhp$E)a@!ZFdn=nIv-*m@Xr^aT3u(l zJdo@#OJDNnE8b$Y#(C7J5Q7WvT6^J}SYxi)0d>MqA&FIih(dS(2hqhHw_ceqRnl2a zh7j==9jhw9!mP2g2KMBJyinnD0IY_Rejcc%eNh5P*(GL6xSXj5b*})KU``?js0?%< z^duG4ISzr0TG#WS8vpF8<*tyX%8|bLDuu(Vyd)+K3d<$*I7`hY zwCD{7B?_nafN9^6dk|iPnR^V>`+8|9UkjpS(5V+tw!RB?R8q<2h^0V{P2Who#_JW% z^uVLdl3~E_r(bkTay#Fe)xGs!Yoc%G+S8lwk6S74icEJEA1UhZJ1de5OW#HDoNAln z<|^QwO92U%CwT7K#kw&S+cEdEcIsHsfum#@zL3KMb-ua2gGE?WeTaIN?$2*W@$s>F zqSoJM(tk>Ro24G_iswat-WlyJeyY%D4sFG;S{wl^!S#w zi!DfbU@g%K=5-F_d&c4&gTPSqTg9`xttmW88$u0P{kNy5|0Tb2&E#P;;$nqf9yW5z zs0z9Ofkk_lD}>#<#3lV{l?B;+pgVK`UmIgWY7;r^XXG*4c2DGEX&rSsM-B^caf+~3 zLbepWh*L#U%ibSq7VCv(QjYsNn~>0*i*|}6uv`$QvQn3&{dAv-I*hg{sxKspX)4_8vW&6+C0V&{czpdYiD^Ey z84;uW=k@uuz%uRdvf{uCi9#lMDv$gsz%kC0V2THKL0;dm=QI9?@g zWPtyLFZF($Mc|iJksB!;{4HUf?xkgc4bpRfbAP6hNMa^QRF?aHZxku^{Xg1spu?r7R+nW`Az|5HF)D*^dOz<#~~kVJ0dQrO^< zH&9u;EsoFg)ZgbvQfe<+vn-Z>aoXU zAOv>jM2i#4VI5|MYil$mE>ba7Iy5UO-j#KprY;9g;4bTdyKY7HWnvBU2cVy^V7NV7Yuz$nCmYgLRyh>!B`@vD!LtgXZ$)h>HJAZQV$-B z@JL6t!{s(b=xIgv-@X53y-w!E&;E(PAu%OJM78pw7EA9rQD-bmMQZ3zb7>=dZ=}v5 z%5yjed&bjyQSFI(^Wa!aOco~xk$s7N>-PQhGR#uvG&ssm?^%qZleOc*t6KG>!9-DK zLSpeas<>s7cWrvq|2~>{`Z3EeMNr}Jf&r{1|1qSo`XqA9Wnp|d z`DAzDP|qIh=FT=vxVxu+orbm(#_kQ0z21UaPc*GNRW#>6Nwk1OEQBG!6g;C&1))Qq zqs;|0So8D(@rlTURow}c38Z~}9x=DiDykI&xi8 z0d}oJVmC->8fyReCqEkdWCgTkz&+$9Hyxuval}b+bnSZTVBV^XrsYI6oyHAJUI?@> zZ&;-tm7{2#$n1i|u@TF4L}sV!(~kfQ3@9=^l+;~q^deHb-0%6yY)hnMSzFo;bGLAl z>sPCWEidCx`-I$5CAHm5jZZ1=qm%29Lu_Sq*(=e=`=bLYMfk!#7Yj@>)ngBM_CV|d zN2+~{rw!r(>+uPgf`#wnK!tPNpV+p9h}wLoO+`9I^_5W@HorRle$323Od{omOd@<) z`pI}s&eUv)Ql&s*zX^#))w>?by`$@X3y3%`cD|CXXhxWb&?3*YtDGGdhCK8dVQsQd z4AuSkJLfb-H<~q>4y^*#cZbC1;%FUn5=FTfH^_T@23YU}*H2rsEolxhO2Ii2AFC+8 zcB*%88ZnB(X{WoLad&EYYuO-bVVto4^toqAm%VRZiMl(&I4~BMfV>TGelnDlfl~!c zVaj|Ya@;5`)J_;aT?3xeRlj>boBRN#VEQ$)Uyye+11D?^N^4sprY6`{gLMpNa8CB=26O{cqg5!TVF z_>c3_Bi2jZvF*qB7Squkm*dv0!cog#hh$BG0^&uwHnD$Vt0`HUqTP&Yd{PPPO9Usu$Z+!B%{dNJ82DZMX> zudw&%NPeU8W>EZL_0nvZ=oNh;B8CIRzP)qT0Mu%+Ak2g;>&eQwq{F4%qxnlBQ~|_Y z(>nG1wT^gD(AVC7mVvWvI9K<>(Y#fxg#@Ns;2eFpDo-%{3_)P|jtvo^7WqdcfRuER z!=DuJl=1tjLLJ7crC}q>-C};1qtN!i7fJo#OKsD8Pk834CTY#_s9zpAbt3;GR#qzI~|F9On>A6iD762yB3wA@*4uUXFKma+JVl!hgFTz1ZHQ7lN z_sdhaeMQ3fd)-*sB!iPC-13mT==2JfKx@C$)%eByu=t9pf$Vq4yUo0+HH6P!lF}i) z4*ebMG)&qf%qZZuYfhnh^v;a-z4_*k^>if2Unb0Bk&-c&Q3B}dh2m Date: Thu, 23 Mar 2023 14:13:10 +0100 Subject: [PATCH 0213/1000] to refresh a token you should not be authenticated --- backend/authentication/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 542a8c8c..c9d83a09 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -54,7 +54,6 @@ def logout(self, request): class RefreshViewHiddenTokens(TokenRefreshView): - permission_classes = [IsAuthenticated] serializer_class = CookieTokenRefreshSerializer def finalize_response(self, request, response, *args, **kwargs): From 3a5a771bcb4dccf5e1e1be751cca162515d26052 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Thu, 23 Mar 2023 14:54:57 +0100 Subject: [PATCH 0214/1000] Moved axios.tsx to lib + removed npm installs from dockerfile --- frontend/Dockerfile | 4 ---- frontend/{pages => lib}/api/axios.tsx | 0 frontend/lib/login.tsx | 2 +- frontend/lib/logout.tsx | 2 +- frontend/lib/reset.tsx | 2 +- frontend/lib/signup.tsx | 2 +- frontend/lib/welcome.tsx | 2 +- 7 files changed, 5 insertions(+), 9 deletions(-) rename frontend/{pages => lib}/api/axios.tsx (100%) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 402def27..99ffcc3b 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -4,10 +4,6 @@ WORKDIR /app/frontend/ COPY package*.json /app/frontend/ RUN npm install -RUN npm install axios -RUN npm install dotenv -RUN npm install bootstrap -RUN npm install --save-dev @types/bootstrap COPY . /app/frontend/ diff --git a/frontend/pages/api/axios.tsx b/frontend/lib/api/axios.tsx similarity index 100% rename from frontend/pages/api/axios.tsx rename to frontend/lib/api/axios.tsx diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index 2eaedcdf..45c01d1b 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -1,4 +1,4 @@ -import api from "../pages/api/axios"; +import api from "./api/axios"; import { Login } from "@/types.d"; import { AxiosResponse } from "axios"; diff --git a/frontend/lib/logout.tsx b/frontend/lib/logout.tsx index 8aa58d96..8040baf3 100644 --- a/frontend/lib/logout.tsx +++ b/frontend/lib/logout.tsx @@ -1,4 +1,4 @@ -import api from "../pages/api/axios"; +import api from "./api/axios"; export const logout = () => { const logout_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_LOGOUT}`; diff --git a/frontend/lib/reset.tsx b/frontend/lib/reset.tsx index 5bf5b095..730b7977 100644 --- a/frontend/lib/reset.tsx +++ b/frontend/lib/reset.tsx @@ -1,5 +1,5 @@ import { Reset_Password } from "@/types.d"; -import api from "@/pages/api/axios"; +import api from "@/lib/api/axios"; import { AxiosResponse } from "axios"; const reset = async (email: string): Promise> => { diff --git a/frontend/lib/signup.tsx b/frontend/lib/signup.tsx index 68a6f395..0992af2e 100644 --- a/frontend/lib/signup.tsx +++ b/frontend/lib/signup.tsx @@ -1,5 +1,5 @@ import { SignUp } from "@/types.d"; -import api from "@/pages/api/axios"; +import api from "@/lib/api/axios"; import { AxiosResponse } from "axios"; const signup = async ( diff --git a/frontend/lib/welcome.tsx b/frontend/lib/welcome.tsx index f9b03824..819cee43 100644 --- a/frontend/lib/welcome.tsx +++ b/frontend/lib/welcome.tsx @@ -1,4 +1,4 @@ -import api from "../pages/api/axios"; +import api from "./api/axios"; export const getAllUsers = () => { const allUsersUrl: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_ALL_USERS}`; From 918cd339a288f045c21a3170d96007d0e804a732 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 15:31:31 +0100 Subject: [PATCH 0215/1000] #88 removed code duplication for @extend_schema --- backend/building/views.py | 14 +++++++------- backend/building_comment/views.py | 10 +++++----- backend/building_on_tour/views.py | 8 ++++---- backend/email_template/views.py | 8 ++++---- backend/garbage_collection/views.py | 8 ++++---- backend/lobby/views.py | 8 ++++---- backend/manual/views.py | 10 +++++----- backend/picture_building/views.py | 8 ++++---- backend/region/views.py | 8 ++++---- backend/role/views.py | 8 ++++---- backend/student_at_building_on_tour/views.py | 8 ++++---- backend/tour/views.py | 8 ++++---- backend/users/views.py | 8 ++++---- backend/util/request_response_util.py | 13 +++++++++++++ frontend/.gitignore | 1 + 15 files changed, 71 insertions(+), 57 deletions(-) diff --git a/backend/building/views.py b/backend/building/views.py index 017ea4d5..04171639 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -16,7 +16,7 @@ class DefaultBuilding(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = BuildingSerializer - @extend_schema(responses={201: BuildingSerializer, 400: None}) + @extend_schema(responses=post_docs(BuildingSerializer)) def post(self, request): """ Create a new building @@ -38,7 +38,7 @@ class BuildingIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding] serializer_class = BuildingSerializer - @extend_schema(responses={200: BuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingSerializer)) def get(self, request, building_id): """ Get info about building with given id @@ -53,7 +53,7 @@ def get(self, request, building_id): serializer = BuildingSerializer(building_instance) return get_success(serializer) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(BuildingSerializer)) def delete(self, request, building_id): """ Delete building with given id @@ -68,7 +68,7 @@ def delete(self, request, building_id): building_instance.delete() return delete_success() - @extend_schema(responses={200: BuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingSerializer)) def patch(self, request, building_id): """ Edit building with given ID @@ -93,7 +93,7 @@ def patch(self, request, building_id): class BuildingPublicView(APIView): serializer_class = BuildingSerializer - @extend_schema(responses={200: BuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingSerializer)) def get(self, request, building_public_id): """ Get building with the public id @@ -115,7 +115,7 @@ class BuildingNewPublicId(APIView): @extend_schema( description="Generate a new unique uuid as public id for the building.", - responses={201: BuildingSerializer, 400: None}, + responses=post_docs(BuildingSerializer), ) def post(self, request, building_id): """ @@ -154,7 +154,7 @@ class BuildingOwnerView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerOfBuilding] serializer_class = BuildingSerializer - @extend_schema(responses={200: BuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingSerializer)) def get(self, request, owner_id): """ Get all buildings owned by syndic with given id diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index b072c76a..bb7ac750 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -14,7 +14,7 @@ class DefaultBuildingComment(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding] serializer_class = BuildingCommentSerializer - @extend_schema(responses={201: BuildingCommentSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) def post(self, request): """ Create a new BuildingComment @@ -37,7 +37,7 @@ class BuildingCommentIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent] serializer_class = BuildingCommentSerializer - @extend_schema(responses={200: BuildingCommentSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) def get(self, request, building_comment_id): """ Get an invividual BuildingComment with given id @@ -51,7 +51,7 @@ def get(self, request, building_comment_id): return get_success(BuildingCommentSerializer(building_comment_instance[0])) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(BuildingCommentSerializer)) def delete(self, request, building_comment_id): """ Delete a BuildingComment with given id @@ -66,7 +66,7 @@ def delete(self, request, building_comment_id): building_comment_instance[0].delete() return delete_success() - @extend_schema(responses={200: BuildingCommentSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) def patch(self, request, building_comment_id): """ Edit BuildingComment with given id @@ -93,7 +93,7 @@ class BuildingCommentBuildingView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent] serializer_class = BuildingCommentSerializer - @extend_schema(responses={200: BuildingCommentSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) def get(self, request, building_id): """ Get all BuildingComments of building with given building id diff --git a/backend/building_on_tour/views.py b/backend/building_on_tour/views.py index 3df577f1..ecdce055 100644 --- a/backend/building_on_tour/views.py +++ b/backend/building_on_tour/views.py @@ -14,7 +14,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = BuildingTourSerializer - @extend_schema(responses={201: BuildingTourSerializer, 400: None}) + @extend_schema(responses=post_docs(BuildingTourSerializer)) def post(self, request): """ Create a new BuildingOnTour with data from post @@ -35,7 +35,7 @@ class BuildingTourIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = BuildingTourSerializer - @extend_schema(responses={200: BuildingTourSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingTourSerializer)) def get(self, request, building_tour_id): """ Get info about a BuildingOnTour with given id @@ -48,7 +48,7 @@ def get(self, request, building_tour_id): serializer = BuildingTourSerializer(building_on_tour_instance[0]) return get_success(serializer) - @extend_schema(responses={200: BuildingTourSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(BuildingTourSerializer)) def patch(self, request, building_tour_id): """ edit info about a BuildingOnTour with given id @@ -69,7 +69,7 @@ def patch(self, request, building_tour_id): return patch_success(BuildingTourSerializer(building_on_tour_instance)) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(BuildingTourSerializer)) def delete(self, request, building_tour_id): """ delete a BuildingOnTour from the database diff --git a/backend/email_template/views.py b/backend/email_template/views.py index 89123716..1fd2f1bf 100644 --- a/backend/email_template/views.py +++ b/backend/email_template/views.py @@ -12,7 +12,7 @@ class DefaultEmailTemplate(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = EmailTemplateSerializer - @extend_schema(responses={201: EmailTemplateSerializer, 400: None}) + @extend_schema(responses=post_docs(EmailTemplateSerializer)) def post(self, request): """ Create a new EmailTemplate @@ -33,7 +33,7 @@ class EmailTemplateIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = EmailTemplateSerializer - @extend_schema(responses={200: EmailTemplateSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(EmailTemplateSerializer)) def get(self, request, email_template_id): """ Get info about an EmailTemplate with given id @@ -45,7 +45,7 @@ def get(self, request, email_template_id): return get_success(EmailTemplateSerializer(email_template_instance[0])) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(EmailTemplateSerializer)) def delete(self, request, email_template_id): """ Delete EmailTemplate with given id @@ -58,7 +58,7 @@ def delete(self, request, email_template_id): email_template_instance[0].delete() return delete_success() - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=get_patch_docs(EmailTemplateSerializer)) def patch(self, request, email_template_id): """ Edit EmailTemplate with given id diff --git a/backend/garbage_collection/views.py b/backend/garbage_collection/views.py index 46b916d9..ca0b416a 100644 --- a/backend/garbage_collection/views.py +++ b/backend/garbage_collection/views.py @@ -14,7 +14,7 @@ class DefaultGarbageCollection(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = GarbageCollectionSerializer - @extend_schema(responses={201: GarbageCollectionSerializer, 400: None}) + @extend_schema(responses=post_docs(GarbageCollectionSerializer)) def post(self, request): """ Create new garbage collection @@ -36,7 +36,7 @@ class GarbageCollectionIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = GarbageCollectionSerializer - @extend_schema(responses={200: GarbageCollectionSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(GarbageCollectionSerializer)) def get(self, request, garbage_collection_id): """ Get info about a garbage collection with given id @@ -47,7 +47,7 @@ def get(self, request, garbage_collection_id): serializer = GarbageCollectionSerializer(garbage_collection_instance[0]) return get_success(serializer) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(GarbageCollectionSerializer)) def delete(self, request, garbage_collection_id): """ Delete garbage collection with given id @@ -58,7 +58,7 @@ def delete(self, request, garbage_collection_id): garbage_collection_instance[0].delete() return delete_success() - @extend_schema(responses={200: GarbageCollectionSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(GarbageCollectionSerializer)) def patch(self, request, garbage_collection_id): """ Edit garbage collection with given id diff --git a/backend/lobby/views.py b/backend/lobby/views.py index 061df992..7900946f 100644 --- a/backend/lobby/views.py +++ b/backend/lobby/views.py @@ -23,7 +23,7 @@ class DefaultLobby(APIView): serializer_class = LobbySerializer @extend_schema( - responses={201: LobbySerializer, 400: None}, + responses=post_docs(LobbySerializer), ) def post(self, request): """ @@ -47,7 +47,7 @@ class LobbyIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = LobbySerializer - @extend_schema(responses={200: LobbySerializer, 400: None}) + @extend_schema(responses=get_patch_docs(LobbySerializer)) def get(self, request, lobby_id): """ Get info about an EmailWhitelist with given id @@ -59,7 +59,7 @@ def get(self, request, lobby_id): return get_success(LobbySerializer(lobby_instance[0])) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(LobbySerializer)) def delete(self, request, lobby_id): """ Patch EmailWhitelist with given id @@ -104,7 +104,7 @@ class LobbyRefreshVerificationCodeView(APIView): @extend_schema( description="Generate a new token. The body of the request is ignored.", request=None, - responses={204: None, 400: None}, + responses=post_docs(LobbySerializer), ) def post(self, request, lobby_id): """ diff --git a/backend/manual/views.py b/backend/manual/views.py index 5b202107..edb2c806 100644 --- a/backend/manual/views.py +++ b/backend/manual/views.py @@ -21,7 +21,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsSyndic] serializer_class = ManualSerializer - @extend_schema(responses={201: ManualSerializer, 400: None}) + @extend_schema(responses=post_docs(ManualSerializer)) def post(self, request): """ Create a new manual with data from post @@ -41,7 +41,7 @@ class ManualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyManualFromSyndic] serializer_class = ManualSerializer - @extend_schema(responses={200: ManualSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(ManualSerializer)) def get(self, request, manual_id): """ Get info about a manual with given id @@ -56,7 +56,7 @@ def get(self, request, manual_id): serializer = ManualSerializer(manual_instances) return get_success(serializer) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(ManualSerializer)) def delete(self, request, manual_id): """ Delete manual with given id @@ -67,7 +67,7 @@ def delete(self, request, manual_id): manual_instances[0].delete() return delete_success() - @extend_schema(responses={200: ManualSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(ManualSerializer)) def patch(self, request, manual_id): """ Edit info about a manual with given id @@ -90,7 +90,7 @@ class ManualBuildingView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding] serializer_class = ManualSerializer - @extend_schema(responses={200: ManualSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(ManualSerializer)) def get(self, request, building_id): """ Get all manuals of a building with given id diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 83636b04..83b0515c 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -14,7 +14,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] serializer_class = PictureBuildingSerializer - @extend_schema(responses={201: PictureBuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(PictureBuildingSerializer)) def post(self, request): """ Create a new PictureBuilding @@ -34,7 +34,7 @@ class PictureBuildingIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding] serializer_class = PictureBuildingSerializer - @extend_schema(responses={200: PictureBuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(PictureBuildingSerializer)) def get(self, request, picture_building_id): """ Get PictureBuilding with given id @@ -50,7 +50,7 @@ def get(self, request, picture_building_id): serializer = PictureBuildingSerializer(picture_building_instance) return get_success(serializer) - @extend_schema(responses={200: PictureBuildingSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(PictureBuildingSerializer)) def patch(self, request, picture_building_id): """ Edit info about PictureBuilding with given id @@ -71,7 +71,7 @@ def patch(self, request, picture_building_id): return patch_success(PictureBuildingSerializer(picture_building_instance)) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(PictureBuildingSerializer)) def delete(self, request, picture_building_id): """ delete a pictureBuilding from the database diff --git a/backend/region/views.py b/backend/region/views.py index 3ca34f74..673a9890 100644 --- a/backend/region/views.py +++ b/backend/region/views.py @@ -12,7 +12,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin] serializer_class = RegionSerializer - @extend_schema(responses={201: RegionSerializer, 400: None}) + @extend_schema(responses=post_docs(RegionSerializer)) def post(self, request): """ Create a new region @@ -34,7 +34,7 @@ class RegionIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | ReadOnly] serializer_class = RegionSerializer - @extend_schema(responses={200: RegionSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(RegionSerializer)) def get(self, request, region_id): """ Get info about a Region with given id @@ -47,7 +47,7 @@ def get(self, request, region_id): serializer = RegionSerializer(region_instance[0]) return get_success(serializer) - @extend_schema(responses={200: RegionSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(RegionSerializer)) def patch(self, request, region_id): """ Edit Region with given id @@ -68,7 +68,7 @@ def patch(self, request, region_id): return patch_success(RegionSerializer(region_instance)) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(RegionSerializer)) def delete(self, request, region_id): """ delete a region with given id diff --git a/backend/role/views.py b/backend/role/views.py index a57218a0..cad23604 100644 --- a/backend/role/views.py +++ b/backend/role/views.py @@ -12,7 +12,7 @@ class DefaultRoleView(APIView): permission_classes = [IsAuthenticated, IsAdmin] serializer_class = RoleSerializer - @extend_schema(responses={201: RoleSerializer, 400: None}) + @extend_schema(responses=post_docs(RoleSerializer)) def post(self, request): """ Create a new role @@ -33,7 +33,7 @@ class RoleIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = RoleSerializer - @extend_schema(responses={200: RoleSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(RoleSerializer)) def get(self, request, role_id): """ Get info about a Role with given id @@ -46,7 +46,7 @@ def get(self, request, role_id): serializer = RoleSerializer(role_instance[0]) return get_success(serializer) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(RoleSerializer)) def delete(self, request, role_id): """ Delete a Role with given id @@ -59,7 +59,7 @@ def delete(self, request, role_id): role_instance[0].delete() return delete_success() - @extend_schema(responses={200: RoleSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(RoleSerializer)) def patch(self, request, role_id): """ Edit info about a Role with given id diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index 794f0571..0c989802 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -14,7 +14,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = StudBuildTourSerializer - @extend_schema(responses={201: StudBuildTourSerializer, 400: None}) + @extend_schema(responses={post_docs(StudBuildTourSerializer)}) def post(self, request): """ Create a new StudentAtBuildingOnTour @@ -50,7 +50,7 @@ class StudentAtBuildingOnTourIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerAccount] serializer_class = StudBuildTourSerializer - @extend_schema(responses={200: StudBuildTourSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(StudBuildTourSerializer)) def get(self, request, student_at_building_on_tour_id): """ Get an individual StudentAtBuildingOnTour with given id @@ -66,7 +66,7 @@ def get(self, request, student_at_building_on_tour_id): serializer = StudBuildTourSerializer(stud_tour_building_instance) return get_success(serializer) - @extend_schema(responses={200: StudBuildTourSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(StudBuildTourSerializer)) def patch(self, request, student_at_building_on_tour_id): """ Edit info about an individual StudentAtBuildingOnTour with given id @@ -90,7 +90,7 @@ def patch(self, request, student_at_building_on_tour_id): serializer = StudBuildTourSerializer(stud_tour_building_instance) return patch_success(serializer) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(StudBuildTourSerializer)) def delete(self, request, student_at_building_on_tour_id): """ Delete StudentAtBuildingOnTour with given id diff --git a/backend/tour/views.py b/backend/tour/views.py index f9392e0e..8f1ac281 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -14,7 +14,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = TourSerializer - @extend_schema(responses={201: TourSerializer, 400: None}) + @extend_schema(responses=post_docs(TourSerializer)) def post(self, request): """ Create a new tour @@ -34,7 +34,7 @@ class TourIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = TourSerializer - @extend_schema(responses={200: TourSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(TourSerializer)) def get(self, request, tour_id): """ Get info about a Tour with given id @@ -48,7 +48,7 @@ def get(self, request, tour_id): serializer = TourSerializer(tour_instance) return get_success(serializer) - @extend_schema(responses={200: TourSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(TourSerializer)) def patch(self, request, tour_id): """ Edit a tour with given id @@ -68,7 +68,7 @@ def patch(self, request, tour_id): return patch_success(TourSerializer(tour_instance)) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(TourSerializer)) def delete(self, request, tour_id): """ Delete a tour with given id diff --git a/backend/users/views.py b/backend/users/views.py index fad84411..8884e5a5 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -46,7 +46,7 @@ class DefaultUser(APIView): # In the future, we probably won't use POST this way anymore (if we work with the whitelist method) # However, an easy workaround would be to add a default value to password (in e.g. `clean`) # -> probably the easiest way - @extend_schema(responses={201: UserSerializer, 400: None}) + @extend_schema(responses=post_docs(UserSerializer)) def post(self, request): """ Create a new user @@ -84,7 +84,7 @@ class UserIndividualView(APIView): ] serializer_class = UserSerializer - @extend_schema(responses={200: UserSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(UserSerializer)) def get(self, request, user_id): """ Get info about user with given id @@ -100,7 +100,7 @@ def get(self, request, user_id): serializer = UserSerializer(user_instance) return get_success(serializer) - @extend_schema(responses={204: None, 400: None}) + @extend_schema(responses=delete_docs(UserSerializer)) def delete(self, request, user_id): """ Delete user with given id @@ -118,7 +118,7 @@ def delete(self, request, user_id): return delete_success() - @extend_schema(responses={200: UserSerializer, 400: None}) + @extend_schema(responses=get_patch_docs(UserSerializer)) def patch(self, request, user_id): """ Edit user with given id diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index b2d62822..5c00ab90 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -91,3 +91,16 @@ def get_success(serializer): def patch_success(serializer): return get_success(serializer) + + +def post_docs(serializer): + return {201: serializer, 400: None} + + +def delete_docs(serializer): + return {204: serializer, 400: None} + + +def get_patch_docs(serializer): + return {200: serializer, 400: None} + diff --git a/frontend/.gitignore b/frontend/.gitignore index c87c9b39..8a1beb08 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -34,3 +34,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +/env.local From 7492d86d11d2d2af1787681e8bc0984f36f2f35c Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 15:50:11 +0100 Subject: [PATCH 0216/1000] #106 edited database.md based on suggestions --- docs/wiki-pages/database.md | 42 +++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/docs/wiki-pages/database.md b/docs/wiki-pages/database.md index 78f17cf1..523c4776 100644 --- a/docs/wiki-pages/database.md +++ b/docs/wiki-pages/database.md @@ -16,17 +16,19 @@ Django migrations are used to propagate changes made in our models ([models.py]( Below we will give a general overview of how they work, but a more detailed guide can be found [here](https://docs.djangoproject.com/en/4.1/topics/migrations/). ### Model migrations +In order to simplify this process a bit, we have provided a script that makes the migrations and migrates them at the same time. +Just use +```bash +$ ./migrations.sh +``` +in the root of the project directory. Make sure that your docker container is running with `docker-compose up`! -To create a new migration based on the changes you have made to the model in [models.py](https://github.com/SELab-2/Dr-Trottoir-4/blob/develop/backend/base/models.py), use the command: -```python manage.py makemigrations``` - -but in our case we need to execute this in our docker container (in the project directory), so we use instead: - +The first line in the script ``` docker-compose exec backend python manage.py makemigrations ``` -Make sure that your docker container is running with `docker-compose up`. +Creates a new migration based on the changes you have made to the model in [models.py](https://github.com/SELab-2/Dr-Trottoir-4/blob/develop/backend/base/models.py). New migrations that are created have a unique identifier, this identifier is just an autoincremented id. @@ -38,29 +40,18 @@ There are already some migration files in our project, these can be found in the **Instead just create a new migrations on top of the already executed migrations.** -The migrations that are created with the commands above, need to be migrated to the database. This is where this command comes in: ``` -python manage.py migrate``` - -Or in our case again inside a docker container: +The migrations that are created with the commands above, need to be migrated to the database. This is what the second command in the script does: ``` docker-compose exec backend python manage.py migrate ``` -In order to simplify this process a bit, we have provided a script that makes the migrations and migrates them at the same time. -Just use -```bash -$ ./migrations.sh -``` -in the root of the project directory. - ### Data migrations We can also use migrations to insert (or remove) data in our database. These are called data migrations, more info can be found [here](https://docs.djangoproject.com/en/4.1/topics/migrations/#data-migrations). +We haven't used this in our project however, instead we have opted for fixtures which will be covered in the next section. +This section about data migrations has only been left in for the sake of completion. To create a new autogenerated data migration we can use the command: -```python manage.py makemigrations --empty yourappname``` - -but because we use docker and our app is called *drtrottoir*, we use the command: ``` docker-compose exec backend python manage.py makemigrations --empty drtrottoir ``` @@ -164,8 +155,8 @@ More information about fixtures can be found [here](https://docs.djangoproject.c ## Viewing the data ### Viewing the data in postgres -This option is mainly meant for whenever you really want to be able to execute SQL and as such, you will probably not need -it much if at all. We'll still cover it however just in case. +You can also connect to the actual database and request the data using SQL queries. However, +you normally don't need this since we usually interact with it through the back-end. To check whether the data has been correctly inserted into the database, we can log onto the postgres docker container: We can do this by first checking our running containers, using the command: @@ -200,4 +191,9 @@ drtrottoir=# \dt ### Viewing the data on the admin page Alternatively, you can head over to the [admin page](http://localhost/api/admin) to get a GUI to view the contents of the -database. You can also easily add, remove and edit entries on the admin page without having to write any SQL code. \ No newline at end of file +database. You can also easily add, remove and edit entries on the admin page without having to write any SQL code. Just +make sure you have a superuser to login to the admin page. You can do this by either running the following command: +```bash +docker-compose exec backend python manage.py createsuperuser +``` +Or using our provided datadump which already has multiple superusers. \ No newline at end of file From 70f6daec47ff0b966dc7712e4daafa84c4fbfa15 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Thu, 23 Mar 2023 15:55:47 +0100 Subject: [PATCH 0217/1000] setup for custom signup serializer --- backend/authentication/serializers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 backend/authentication/serializers.py diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py new file mode 100644 index 00000000..3e0e1b5b --- /dev/null +++ b/backend/authentication/serializers.py @@ -0,0 +1,12 @@ +from dj_rest_auth.registration.serializers import RegisterSerializer +from rest_framework import serializers + + +class CustomRegisterSerializer(RegisterSerializer): + first_name = serializers.CharField(required=True) + last_name = serializers.CharField(required=True) + verification_code = serializers.CharField(required=True) + + def custom_signup(self, request, user): + pass + From 663416abfededbc88f67e2a9c84369304bba986a Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 16:06:21 +0100 Subject: [PATCH 0218/1000] #106 edited database.md --- docs/wiki-pages/database.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/wiki-pages/database.md b/docs/wiki-pages/database.md index 523c4776..b5669d63 100644 --- a/docs/wiki-pages/database.md +++ b/docs/wiki-pages/database.md @@ -30,12 +30,12 @@ docker-compose exec backend python manage.py makemigrations Creates a new migration based on the changes you have made to the model in [models.py](https://github.com/SELab-2/Dr-Trottoir-4/blob/develop/backend/base/models.py). -New migrations that are created have a unique identifier, this identifier is just an autoincremented id. +New migrations that are created have a unique identifier, which is just an auto incremented id. There are already some migration files in our project, these can be found in the [migrations](https://github.com/SELab-2/Dr-Trottoir-4/tree/develop/backend/base/migrations) folder. **Warning**: -**The filenames and content of these files can never be changed, as this will cause problems for those who already executed these migrations!** +**The filenames and content of these files must never be changed, as this will cause problems for those who already executed these migrations!** > This is because django keeps track of a migration history inside the database, in this table the filename is tracked. **Instead just create a new migrations on top of the already executed migrations.** @@ -47,7 +47,8 @@ docker-compose exec backend python manage.py migrate ``` ### Data migrations -We can also use migrations to insert (or remove) data in our database. These are called data migrations, more info can be found [here](https://docs.djangoproject.com/en/4.1/topics/migrations/#data-migrations). +We can also use migrations to insert (or remove) data in our database. These are called data migrations, more information +about them can be found [here](https://docs.djangoproject.com/en/4.1/topics/migrations/#data-migrations). We haven't used this in our project however, instead we have opted for fixtures which will be covered in the next section. This section about data migrations has only been left in for the sake of completion. @@ -63,7 +64,7 @@ docker-compose exec backend python manage.py migrate ``` ## Fixtures -**Fixtures** in django are another useful way to dump data and add the data to our database, and they are our preferred +**Fixtures** in Django are another useful way to dump data and add the data to our database, and they are our preferred method. An example of a fixtures file is [datadump.json](https://github.com/SELab-2/Dr-Trottoir-4/blob/develop/backend/datadump.json). In order to create a data dump of our database file using docker, we can use the command: @@ -196,4 +197,4 @@ make sure you have a superuser to login to the admin page. You can do this by ei ```bash docker-compose exec backend python manage.py createsuperuser ``` -Or using our provided datadump which already has multiple superusers. \ No newline at end of file +which makes a new superuser, or by using our provided datadump which already has multiple superusers. \ No newline at end of file From 58d161213ef3a59b037acc1da3947a674cba63ee Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 16:12:11 +0100 Subject: [PATCH 0219/1000] #106 changed drtrottoir to base in sql command in database.md --- docs/wiki-pages/database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wiki-pages/database.md b/docs/wiki-pages/database.md index b5669d63..9fa10636 100644 --- a/docs/wiki-pages/database.md +++ b/docs/wiki-pages/database.md @@ -182,7 +182,7 @@ Now if we want to do any operation in SQL, we can do so. The tables that are cre For example if we want the rows of our users, we can type: ``` -drtrottoir=# SELECT * from drtrottoir_user; +drtrottoir=# SELECT * from base_user; ``` To get a list of all tables created by django, you can use the command: From 1a98e3fde507afe2fed13a9daca0bf92008440d8 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 16:16:25 +0100 Subject: [PATCH 0220/1000] #106 updated README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0a886e26..9d721772 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ run the following command in the frontend folder: npm install next ``` -Now we are ready to run the full application. We can do this by building it first: +Now we are ready to run the full application. To run the docker container you can use the following command: ```bash -docker-compose build +docker-compose up ``` -Followed by: +Whenever you need to rebuild your containers, just use: ```bash -docker-compose up +docker-compose build ``` To stop the containers, run `docker-compose down` or press `Ctrl+C` if the process is running in the foreground. From 3da2671979f258d6cbdeac955dab796ab643be34 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 16:19:12 +0100 Subject: [PATCH 0221/1000] #106 updated README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 9d721772..e63fd940 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ Whenever you need to rebuild your containers, just use: docker-compose build ``` +Or if you want to build and then run at the same time, use: +```bash +docker-compose up --build -d +``` + To stop the containers, run `docker-compose down` or press `Ctrl+C` if the process is running in the foreground. Alternatively, you can use the stop button in Docker Desktop. From 24549cdf905c8b79152c4b58d54cd7adc23482f0 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 17:21:58 +0100 Subject: [PATCH 0222/1000] #88 fixed some wrong response codes --- backend/building_comment/views.py | 2 +- backend/picture_building/views.py | 2 +- backend/student_at_building_on_tour/views.py | 2 +- backend/util/request_response_util.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index bb7ac750..b943c4f0 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -14,7 +14,7 @@ class DefaultBuildingComment(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding] serializer_class = BuildingCommentSerializer - @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) + @extend_schema(responses=post_docs(BuildingCommentSerializer)) def post(self, request): """ Create a new BuildingComment diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 83b0515c..81553d0e 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -14,7 +14,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] serializer_class = PictureBuildingSerializer - @extend_schema(responses=get_patch_docs(PictureBuildingSerializer)) + @extend_schema(responses=post_docs(PictureBuildingSerializer)) def post(self, request): """ Create a new PictureBuilding diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index 0c989802..3cbf18a7 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -14,7 +14,7 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = StudBuildTourSerializer - @extend_schema(responses={post_docs(StudBuildTourSerializer)}) + @extend_schema(responses=post_docs(StudBuildTourSerializer)) def post(self, request): """ Create a new StudentAtBuildingOnTour diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index 5c00ab90..c54fde7b 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -98,7 +98,7 @@ def post_docs(serializer): def delete_docs(serializer): - return {204: serializer, 400: None} + return {204: None, 400: None} def get_patch_docs(serializer): From 58a1a1d0bdd45d1a09ed970c83bdb7ee82516bac Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 17:41:11 +0100 Subject: [PATCH 0223/1000] #88 removed argument for delete_docs() and fixed typo --- backend/building/views.py | 2 +- backend/building_comment/views.py | 2 +- backend/building_on_tour/views.py | 2 +- backend/email_template/views.py | 2 +- backend/garbage_collection/views.py | 2 +- backend/lobby/views.py | 2 +- backend/manual/views.py | 2 +- backend/picture_building/views.py | 2 +- backend/region/views.py | 2 +- backend/role/views.py | 2 +- backend/student_at_building_on_tour/views.py | 2 +- backend/tour/views.py | 2 +- backend/users/views.py | 4 ++-- backend/util/request_response_util.py | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/building/views.py b/backend/building/views.py index 04171639..f75e3805 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -53,7 +53,7 @@ def get(self, request, building_id): serializer = BuildingSerializer(building_instance) return get_success(serializer) - @extend_schema(responses=delete_docs(BuildingSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, building_id): """ Delete building with given id diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index b943c4f0..b35ff5ba 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -51,7 +51,7 @@ def get(self, request, building_comment_id): return get_success(BuildingCommentSerializer(building_comment_instance[0])) - @extend_schema(responses=delete_docs(BuildingCommentSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, building_comment_id): """ Delete a BuildingComment with given id diff --git a/backend/building_on_tour/views.py b/backend/building_on_tour/views.py index ecdce055..d83aa7c1 100644 --- a/backend/building_on_tour/views.py +++ b/backend/building_on_tour/views.py @@ -69,7 +69,7 @@ def patch(self, request, building_tour_id): return patch_success(BuildingTourSerializer(building_on_tour_instance)) - @extend_schema(responses=delete_docs(BuildingTourSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, building_tour_id): """ delete a BuildingOnTour from the database diff --git a/backend/email_template/views.py b/backend/email_template/views.py index 1fd2f1bf..d90e1b31 100644 --- a/backend/email_template/views.py +++ b/backend/email_template/views.py @@ -45,7 +45,7 @@ def get(self, request, email_template_id): return get_success(EmailTemplateSerializer(email_template_instance[0])) - @extend_schema(responses=delete_docs(EmailTemplateSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, email_template_id): """ Delete EmailTemplate with given id diff --git a/backend/garbage_collection/views.py b/backend/garbage_collection/views.py index ca0b416a..e2304ba7 100644 --- a/backend/garbage_collection/views.py +++ b/backend/garbage_collection/views.py @@ -47,7 +47,7 @@ def get(self, request, garbage_collection_id): serializer = GarbageCollectionSerializer(garbage_collection_instance[0]) return get_success(serializer) - @extend_schema(responses=delete_docs(GarbageCollectionSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, garbage_collection_id): """ Delete garbage collection with given id diff --git a/backend/lobby/views.py b/backend/lobby/views.py index 7900946f..4116422e 100644 --- a/backend/lobby/views.py +++ b/backend/lobby/views.py @@ -59,7 +59,7 @@ def get(self, request, lobby_id): return get_success(LobbySerializer(lobby_instance[0])) - @extend_schema(responses=delete_docs(LobbySerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, lobby_id): """ Patch EmailWhitelist with given id diff --git a/backend/manual/views.py b/backend/manual/views.py index edb2c806..c2dba7f8 100644 --- a/backend/manual/views.py +++ b/backend/manual/views.py @@ -56,7 +56,7 @@ def get(self, request, manual_id): serializer = ManualSerializer(manual_instances) return get_success(serializer) - @extend_schema(responses=delete_docs(ManualSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, manual_id): """ Delete manual with given id diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 81553d0e..02259846 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -71,7 +71,7 @@ def patch(self, request, picture_building_id): return patch_success(PictureBuildingSerializer(picture_building_instance)) - @extend_schema(responses=delete_docs(PictureBuildingSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, picture_building_id): """ delete a pictureBuilding from the database diff --git a/backend/region/views.py b/backend/region/views.py index 673a9890..1158aff6 100644 --- a/backend/region/views.py +++ b/backend/region/views.py @@ -68,7 +68,7 @@ def patch(self, request, region_id): return patch_success(RegionSerializer(region_instance)) - @extend_schema(responses=delete_docs(RegionSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, region_id): """ delete a region with given id diff --git a/backend/role/views.py b/backend/role/views.py index cad23604..dcd786f3 100644 --- a/backend/role/views.py +++ b/backend/role/views.py @@ -46,7 +46,7 @@ def get(self, request, role_id): serializer = RoleSerializer(role_instance[0]) return get_success(serializer) - @extend_schema(responses=delete_docs(RoleSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, role_id): """ Delete a Role with given id diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index 3cbf18a7..b3ad8423 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -90,7 +90,7 @@ def patch(self, request, student_at_building_on_tour_id): serializer = StudBuildTourSerializer(stud_tour_building_instance) return patch_success(serializer) - @extend_schema(responses=delete_docs(StudBuildTourSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, student_at_building_on_tour_id): """ Delete StudentAtBuildingOnTour with given id diff --git a/backend/tour/views.py b/backend/tour/views.py index 8f1ac281..65bcf474 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -68,7 +68,7 @@ def patch(self, request, tour_id): return patch_success(TourSerializer(tour_instance)) - @extend_schema(responses=delete_docs(TourSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, tour_id): """ Delete a tour with given id diff --git a/backend/users/views.py b/backend/users/views.py index 8884e5a5..ac9f1cdf 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -100,11 +100,11 @@ def get(self, request, user_id): serializer = UserSerializer(user_instance) return get_success(serializer) - @extend_schema(responses=delete_docs(UserSerializer)) + @extend_schema(responses=delete_docs()) def delete(self, request, user_id): """ Delete user with given id - We don't acutally delete a user, we put the user on inactive mode + We don't actually delete a user, we put the user on inactive mode """ user_instance = User.objects.filter(id=user_id) if not user_instance: diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index c54fde7b..84422baa 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -97,7 +97,7 @@ def post_docs(serializer): return {201: serializer, 400: None} -def delete_docs(serializer): +def delete_docs(): return {204: None, 400: None} From 283a5be3fd4dbe9412a27d7145a239545af80bc2 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 18:07:26 +0100 Subject: [PATCH 0224/1000] #88 removed schema.yml --- backend/schema.yml | 2904 -------------------------------------------- 1 file changed, 2904 deletions(-) delete mode 100644 backend/schema.yml diff --git a/backend/schema.yml b/backend/schema.yml deleted file mode 100644 index f3c79439..00000000 --- a/backend/schema.yml +++ /dev/null @@ -1,2904 +0,0 @@ -openapi: 3.0.3 -info: - title: Dr-Trottoir API - version: 1.0.0 - description: This is the documentation for the Dr-Trottoir API -paths: - /: - get: - operationId: root_retrieve - description: If you are logged in, you should see "Hello from the DrTrottoir - API!". - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - description: No response body - '400': - description: No response body - '403': - description: No response body - '401': - description: No response body - /authentication/account-confirm-email/: - post: - operationId: authentication_account_confirm_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/account-confirm-email/{key}/: - post: - operationId: authentication_account_confirm_email_create_2 - parameters: - - in: path - name: key - schema: - type: string - required: true - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/login/: - post: - operationId: authentication_login_create - description: |- - Check the credentials and return the REST Token - if the credentials are valid and authenticated. - Calls Django Auth login method to register User ID - in Django session framework - - Accept the following POST parameters: username, password - Return the REST Framework Token Object's key. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Login' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Login' - multipart/form-data: - schema: - $ref: '#/components/schemas/Login' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Login' - description: '' - /authentication/logout/: - get: - operationId: authentication_logout_retrieve - description: |- - Calls Django logout method and delete the Token object - assigned to the current User object. - - Accepts/Returns nothing. - tags: - - authentication - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - description: '' - post: - operationId: authentication_logout_create - description: |- - Calls Django logout method and delete the Token object - assigned to the current User object. - - Accepts/Returns nothing. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TokenRefresh' - multipart/form-data: - schema: - $ref: '#/components/schemas/TokenRefresh' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - description: '' - /authentication/password/change/: - post: - operationId: authentication_password_change_create - description: |- - Calls Django Auth SetPasswordForm save method. - - Accepts the following POST parameters: new_password1, new_password2 - Returns the success/fail message. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PasswordChange' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PasswordChange' - multipart/form-data: - schema: - $ref: '#/components/schemas/PasswordChange' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/password/reset/: - post: - operationId: authentication_password_reset_create - description: |- - Calls Django Auth PasswordResetForm save method. - - Accepts the following POST parameters: email - Returns the success/fail message. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PasswordReset' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PasswordReset' - multipart/form-data: - schema: - $ref: '#/components/schemas/PasswordReset' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/password/reset/confirm/{uidb64}/{token}/: - post: - operationId: authentication_password_reset_confirm_create - description: |- - Password reset e-mail link is confirmed, therefore - this resets the user's password. - - Accepts the following POST parameters: token, uid, - new_password1, new_password2 - Returns the success/fail message. - parameters: - - in: path - name: token - schema: - type: string - required: true - - in: path - name: uidb64 - schema: - type: string - required: true - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PasswordResetConfirm' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PasswordResetConfirm' - multipart/form-data: - schema: - $ref: '#/components/schemas/PasswordResetConfirm' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/signup/: - post: - operationId: authentication_signup_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Register' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Register' - multipart/form-data: - schema: - $ref: '#/components/schemas/Register' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/JWT' - description: '' - /authentication/signup/resend-email/: - post: - operationId: authentication_signup_resend_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ResendEmailVerification' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/ResendEmailVerification' - multipart/form-data: - schema: - $ref: '#/components/schemas/ResendEmailVerification' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/signup/verify-email/: - post: - operationId: authentication_signup_verify_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/token/refresh/: - post: - operationId: authentication_token_refresh_create - description: |- - Takes a refresh type JSON web token and returns an access type JSON web - token if the refresh token is valid. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TokenRefresh' - multipart/form-data: - schema: - $ref: '#/components/schemas/TokenRefresh' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - description: '' - /authentication/token/verify/: - post: - operationId: authentication_token_verify_create - description: |- - Takes a token and indicates if it is valid. This view provides no - information about a token's fitness for a particular use. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TokenVerify' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TokenVerify' - multipart/form-data: - schema: - $ref: '#/components/schemas/TokenVerify' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenVerify' - description: '' - /authentication/verify-email/: - post: - operationId: authentication_verify_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /building/: - post: - operationId: building_create - description: Create a new building - tags: - - building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Building' - multipart/form-data: - schema: - $ref: '#/components/schemas/Building' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - /building-comment/: - post: - operationId: building_comment_create - description: Create a new BuildingComment - tags: - - building-comment - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/BuildingComment' - multipart/form-data: - schema: - $ref: '#/components/schemas/BuildingComment' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - /building-comment/{building_comment_id}/: - get: - operationId: building_comment_retrieve - description: Get an invividual BuildingComment with given id - parameters: - - in: path - name: building_comment_id - schema: - type: integer - required: true - tags: - - building-comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - patch: - operationId: building_comment_partial_update - description: Edit BuildingComment with given id - parameters: - - in: path - name: building_comment_id - schema: - type: integer - required: true - tags: - - building-comment - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuildingComment' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuildingComment' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuildingComment' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - delete: - operationId: building_comment_destroy - description: Delete a BuildingComment with given id - parameters: - - in: path - name: building_comment_id - schema: - type: integer - required: true - tags: - - building-comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /building-comment/all/: - get: - operationId: building_comment_all_retrieve - description: Get all BuildingComments in database - tags: - - building-comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - /building-comment/building/{building_id}/: - get: - operationId: building_comment_building_retrieve - description: Get all BuildingComments of building with given building id - parameters: - - in: path - name: building_id - schema: - type: string - required: true - tags: - - building-comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - /building-on-tour/: - post: - operationId: building_on_tour_create - description: Create a new BuildingOnTour with data from post - tags: - - building-on-tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/BuildingTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/BuildingTour' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - '400': - description: No response body - /building-on-tour/{building_tour_id}/: - get: - operationId: building_on_tour_retrieve - description: Get info about a BuildingOnTour with given id - parameters: - - in: path - name: building_tour_id - schema: - type: integer - required: true - tags: - - building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - '400': - description: No response body - patch: - operationId: building_on_tour_partial_update - description: edit info about a BuildingOnTour with given id - parameters: - - in: path - name: building_tour_id - schema: - type: integer - required: true - tags: - - building-on-tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuildingTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuildingTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuildingTour' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - '400': - description: No response body - delete: - operationId: building_on_tour_destroy - description: delete a BuildingOnTour from the database - parameters: - - in: path - name: building_tour_id - schema: - type: integer - required: true - tags: - - building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /building-on-tour/all/: - get: - operationId: building_on_tour_all_retrieve - description: Get all buildings on tours - tags: - - building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - /building/{building_id}/: - get: - operationId: building_retrieve - description: Get info about building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - patch: - operationId: building_partial_update - description: Edit building with given ID - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuilding' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuilding' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuilding' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - delete: - operationId: building_destroy - description: Delete building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /building/all/: - get: - operationId: building_all_retrieve - description: Get all buildings - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - /building/new-public-id/{building_id}/: - post: - operationId: building_new_public_id_create - description: Generate a new unique uuid as public id for the building. - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Building' - multipart/form-data: - schema: - $ref: '#/components/schemas/Building' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - /building/owner/{owner_id}/: - get: - operationId: building_owner_retrieve - description: Get all buildings owned by syndic with given id - parameters: - - in: path - name: owner_id - schema: - type: integer - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - /building/public/{building_public_id}/: - get: - operationId: building_public_retrieve - description: Get building with the public id - parameters: - - in: path - name: building_public_id - schema: - type: string - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - /email-template/: - post: - operationId: email_template_create - description: Create a new EmailTemplate - tags: - - email-template - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/EmailTemplate' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/EmailTemplate' - multipart/form-data: - schema: - $ref: '#/components/schemas/EmailTemplate' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/EmailTemplate' - description: '' - '400': - description: No response body - /email-template/{email_template_id}/: - get: - operationId: email_template_retrieve - description: Get info about an EmailTemplate with given id - parameters: - - in: path - name: email_template_id - schema: - type: integer - required: true - tags: - - email-template - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/EmailTemplate' - description: '' - '400': - description: No response body - patch: - operationId: email_template_partial_update - description: Edit EmailTemplate with given id - parameters: - - in: path - name: email_template_id - schema: - type: integer - required: true - tags: - - email-template - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedEmailTemplate' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedEmailTemplate' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedEmailTemplate' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - delete: - operationId: email_template_destroy - description: Delete EmailTemplate with given id - parameters: - - in: path - name: email_template_id - schema: - type: integer - required: true - tags: - - email-template - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /email-template/all/: - get: - operationId: email_template_all_retrieve - description: Get all EmailTemplates in the database - tags: - - email-template - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/EmailTemplate' - description: '' - /garbage-collection/: - post: - operationId: garbage_collection_create - description: Create new garbage collection - tags: - - garbage-collection - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/GarbageCollection' - multipart/form-data: - schema: - $ref: '#/components/schemas/GarbageCollection' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - '400': - description: No response body - /garbage-collection/{garbage_collection_id}/: - get: - operationId: garbage_collection_retrieve - description: Get info about a garbage collection with given id - parameters: - - in: path - name: garbage_collection_id - schema: - type: string - required: true - tags: - - garbage-collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - '400': - description: No response body - patch: - operationId: garbage_collection_partial_update - description: Edit garbage collection with given id - parameters: - - in: path - name: garbage_collection_id - schema: - type: string - required: true - tags: - - garbage-collection - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedGarbageCollection' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedGarbageCollection' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedGarbageCollection' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - '400': - description: No response body - delete: - operationId: garbage_collection_destroy - description: Delete garbage collection with given id - parameters: - - in: path - name: garbage_collection_id - schema: - type: string - required: true - tags: - - garbage-collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /garbage-collection/all/: - get: - operationId: garbage_collection_all_retrieve - description: Get all garbage collections - tags: - - garbage-collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - /garbage-collection/building/{building_id}/: - get: - operationId: garbage_collection_building_retrieve - description: Get info about all garbage collections of a building with given - id - parameters: - - in: path - name: building_id - schema: - type: string - required: true - tags: - - garbage-collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - /lobby/: - post: - operationId: lobby_create - description: Create a new whitelisted email - tags: - - lobby - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Lobby' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Lobby' - multipart/form-data: - schema: - $ref: '#/components/schemas/Lobby' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Lobby' - description: '' - '400': - description: No response body - /lobby/{email_whitelist_id}/: - get: - operationId: lobby_retrieve - description: Get info about an EmailWhitelist with given id - parameters: - - in: path - name: email_whitelist_id - schema: - type: integer - required: true - tags: - - lobby - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Lobby' - description: '' - '400': - description: No response body - delete: - operationId: lobby_destroy - description: Patch EmailWhitelist with given id - parameters: - - in: path - name: email_whitelist_id - schema: - type: integer - required: true - tags: - - lobby - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /lobby/all/: - get: - operationId: lobby_all_retrieve - description: Get info about the EmailWhiteList with given id - tags: - - lobby - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Lobby' - description: '' - /lobby/new-verification-code/{lobby_id}/: - post: - operationId: lobby_new_verification_code_create - description: Generate a new token. The body of the request is ignored. - parameters: - - in: path - name: lobby_id - schema: - type: string - required: true - tags: - - lobby - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /manual/: - post: - operationId: manual_create - description: Create a new manual with data from post - tags: - - manual - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Manual' - multipart/form-data: - schema: - $ref: '#/components/schemas/Manual' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - /manual/{manual_id}/: - get: - operationId: manual_retrieve - description: Get info about a manual with given id - parameters: - - in: path - name: manual_id - schema: - type: integer - required: true - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - patch: - operationId: manual_partial_update - description: Edit info about a manual with given id - parameters: - - in: path - name: manual_id - schema: - type: integer - required: true - tags: - - manual - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedManual' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedManual' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedManual' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - delete: - operationId: manual_destroy - description: Delete manual with given id - parameters: - - in: path - name: manual_id - schema: - type: integer - required: true - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /manual/all/: - get: - operationId: manual_all_retrieve - description: Get all manuals - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - /manual/building/{building_id}/: - get: - operationId: manual_building_retrieve - description: Get all manuals of a building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - /picture-building/: - post: - operationId: picture_building_create - description: Create a new PictureBuilding - tags: - - picture-building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PictureBuilding' - multipart/form-data: - schema: - $ref: '#/components/schemas/PictureBuilding' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - '400': - description: No response body - /picture-building/{picture_building_id}/: - get: - operationId: picture_building_retrieve - description: Get PictureBuilding with given id - parameters: - - in: path - name: picture_building_id - schema: - type: integer - required: true - tags: - - picture-building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - '400': - description: No response body - patch: - operationId: picture_building_partial_update - description: Edit info about PictureBuilding with given id - parameters: - - in: path - name: picture_building_id - schema: - type: integer - required: true - tags: - - picture-building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedPictureBuilding' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedPictureBuilding' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedPictureBuilding' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - '400': - description: No response body - delete: - operationId: picture_building_destroy - description: delete a pictureBuilding from the database - parameters: - - in: path - name: picture_building_id - schema: - type: integer - required: true - tags: - - picture-building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /picture-building/all/: - get: - operationId: picture_building_all_retrieve - description: Get all pictureBuilding - tags: - - picture-building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - /picture-building/building/{building_id}/: - get: - operationId: picture_building_building_retrieve - description: Get all pictures of a building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - picture-building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - /region/: - post: - operationId: region_create - description: Create a new region - tags: - - region - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Region' - multipart/form-data: - schema: - $ref: '#/components/schemas/Region' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - '400': - description: No response body - /region/{region_id}/: - get: - operationId: region_retrieve - description: Get info about a Region with given id - parameters: - - in: path - name: region_id - schema: - type: integer - required: true - tags: - - region - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - '400': - description: No response body - patch: - operationId: region_partial_update - description: Edit Region with given id - parameters: - - in: path - name: region_id - schema: - type: integer - required: true - tags: - - region - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedRegion' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedRegion' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedRegion' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - '400': - description: No response body - delete: - operationId: region_destroy - description: delete a region with given id - parameters: - - in: path - name: region_id - schema: - type: integer - required: true - tags: - - region - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /region/all/: - get: - operationId: region_all_retrieve - description: Get all regions - tags: - - region - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - /role/: - post: - operationId: role_create - description: Create a new role - tags: - - role - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Role' - multipart/form-data: - schema: - $ref: '#/components/schemas/Role' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - '400': - description: No response body - /role/{role_id}/: - get: - operationId: role_retrieve - description: Get info about a Role with given id - parameters: - - in: path - name: role_id - schema: - type: integer - required: true - tags: - - role - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - '400': - description: No response body - patch: - operationId: role_partial_update - description: Edit info about a Role with given id - parameters: - - in: path - name: role_id - schema: - type: integer - required: true - tags: - - role - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedRole' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedRole' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedRole' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - '400': - description: No response body - delete: - operationId: role_destroy - description: Delete a Role with given id - parameters: - - in: path - name: role_id - schema: - type: integer - required: true - tags: - - role - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /role/all/: - get: - operationId: role_all_retrieve - description: Get all roles - tags: - - role - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - /student-at-building-on-tour/: - post: - operationId: student_at_building_on_tour_create - description: Create a new StudentAtBuildingOnTour - tags: - - student-at-building-on-tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/StudBuildTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/StudBuildTour' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - '400': - description: No response body - /student-at-building-on-tour/{student_at_building_on_tour_id}/: - get: - operationId: student_at_building_on_tour_retrieve - description: Get an individual StudentAtBuildingOnTour with given id - parameters: - - in: path - name: student_at_building_on_tour_id - schema: - type: integer - required: true - tags: - - student-at-building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - '400': - description: No response body - patch: - operationId: student_at_building_on_tour_partial_update - description: Edit info about an individual StudentAtBuildingOnTour with given - id - parameters: - - in: path - name: student_at_building_on_tour_id - schema: - type: integer - required: true - tags: - - student-at-building-on-tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedStudBuildTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedStudBuildTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedStudBuildTour' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - '400': - description: No response body - delete: - operationId: student_at_building_on_tour_destroy - description: Delete StudentAtBuildingOnTour with given id - parameters: - - in: path - name: student_at_building_on_tour_id - schema: - type: integer - required: true - tags: - - student-at-building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /student-at-building-on-tour/all/: - get: - operationId: student_at_building_on_tour_all_retrieve - description: Get all StudentAtBuildingOnTours - tags: - - student-at-building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - /student-at-building-on-tour/student/{student_id}/: - get: - operationId: student_at_building_on_tour_student_retrieve - description: Get all StudentAtBuildingOnTour for a student with given id - parameters: - - in: path - name: student_id - schema: - type: integer - required: true - tags: - - student-at-building-on-tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - /tour/: - post: - operationId: tour_create - description: Create a new tour - tags: - - tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Tour' - multipart/form-data: - schema: - $ref: '#/components/schemas/Tour' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - '400': - description: No response body - /tour/{tour_id}/: - get: - operationId: tour_retrieve - description: Get info about a Tour with given id - parameters: - - in: path - name: tour_id - schema: - type: integer - required: true - tags: - - tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - '400': - description: No response body - patch: - operationId: tour_partial_update - description: Edit a tour with given id - parameters: - - in: path - name: tour_id - schema: - type: integer - required: true - tags: - - tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedTour' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - '400': - description: No response body - delete: - operationId: tour_destroy - description: Delete a tour with given id - parameters: - - in: path - name: tour_id - schema: - type: integer - required: true - tags: - - tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /tour/all/: - get: - operationId: tour_all_retrieve - description: Get all tours - tags: - - tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - /user/: - post: - operationId: user_create - description: Create a new user - tags: - - user - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/User' - multipart/form-data: - schema: - $ref: '#/components/schemas/User' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' - '400': - description: No response body - /user/{user_id}/: - get: - operationId: user_retrieve - description: Get info about user with given id - parameters: - - in: path - name: user_id - schema: - type: integer - required: true - tags: - - user - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' - '400': - description: No response body - patch: - operationId: user_partial_update - description: Edit user with given id - parameters: - - in: path - name: user_id - schema: - type: integer - required: true - tags: - - user - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedUser' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedUser' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedUser' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' - '400': - description: No response body - delete: - operationId: user_destroy - description: |- - Delete user with given id - We don't acutally delete a user, we put the user on inactive mode - parameters: - - in: path - name: user_id - schema: - type: integer - required: true - tags: - - user - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /user/all/: - get: - operationId: user_all_retrieve - description: Get all users - tags: - - user - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' -components: - schemas: - Building: - type: object - properties: - id: - type: integer - readOnly: true - city: - type: string - maxLength: 40 - postal_code: - type: string - maxLength: 10 - street: - type: string - maxLength: 60 - house_number: - type: integer - maximum: 2147483647 - minimum: 0 - bus: - type: string - nullable: true - maxLength: 10 - client_number: - type: string - nullable: true - maxLength: 40 - duration: - type: string - format: time - syndic: - type: integer - nullable: true - region: - type: integer - nullable: true - name: - type: string - nullable: true - maxLength: 100 - public_id: - type: string - nullable: true - maxLength: 32 - required: - - city - - house_number - - id - - postal_code - - street - BuildingComment: - type: object - properties: - id: - type: integer - readOnly: true - comment: - type: string - date: - type: string - format: date-time - building: - type: integer - required: - - building - - comment - - date - - id - BuildingTour: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - tour: - type: integer - index: - type: integer - maximum: 2147483647 - minimum: 0 - required: - - building - - id - - index - - tour - EmailTemplate: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 40 - template: - type: string - required: - - id - - name - - template - GarbageCollection: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - date: - type: string - format: date - garbage_type: - $ref: '#/components/schemas/GarbageTypeEnum' - required: - - building - - date - - garbage_type - - id - GarbageTypeEnum: - enum: - - GFT - - GLS - - GRF - - KER - - PAP - - PMD - - RES - type: string - description: |- - * `GFT` - GFT - * `GLS` - Glas - * `GRF` - Grof vuil - * `KER` - Kerstbomen - * `PAP` - Papier - * `PMD` - PMD - * `RES` - Restafval - JWT: - type: object - description: Serializer for JWT authentication. - properties: - access_token: - type: string - refresh_token: - type: string - user: - $ref: '#/components/schemas/User' - required: - - access_token - - refresh_token - - user - Lobby: - type: object - properties: - id: - type: integer - readOnly: true - email: - type: string - format: email - title: Email address - maxLength: 254 - role: - type: integer - nullable: true - required: - - email - - id - Login: - type: object - properties: - username: - type: string - email: - type: string - format: email - password: - type: string - required: - - password - Manual: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - version_number: - type: integer - maximum: 2147483647 - minimum: 0 - file: - type: string - format: uri - nullable: true - required: - - building - - id - PasswordChange: - type: object - properties: - new_password1: - type: string - maxLength: 128 - new_password2: - type: string - maxLength: 128 - required: - - new_password1 - - new_password2 - PasswordReset: - type: object - description: Serializer for requesting a password reset e-mail. - properties: - email: - type: string - format: email - required: - - email - PasswordResetConfirm: - type: object - description: Serializer for confirming a password reset attempt. - properties: - new_password1: - type: string - maxLength: 128 - new_password2: - type: string - maxLength: 128 - uid: - type: string - token: - type: string - required: - - new_password1 - - new_password2 - - token - - uid - PatchedBuilding: - type: object - properties: - id: - type: integer - readOnly: true - city: - type: string - maxLength: 40 - postal_code: - type: string - maxLength: 10 - street: - type: string - maxLength: 60 - house_number: - type: integer - maximum: 2147483647 - minimum: 0 - bus: - type: string - nullable: true - maxLength: 10 - client_number: - type: string - nullable: true - maxLength: 40 - duration: - type: string - format: time - syndic: - type: integer - nullable: true - region: - type: integer - nullable: true - name: - type: string - nullable: true - maxLength: 100 - public_id: - type: string - nullable: true - maxLength: 32 - PatchedBuildingComment: - type: object - properties: - id: - type: integer - readOnly: true - comment: - type: string - date: - type: string - format: date-time - building: - type: integer - PatchedBuildingTour: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - tour: - type: integer - index: - type: integer - maximum: 2147483647 - minimum: 0 - PatchedEmailTemplate: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 40 - template: - type: string - PatchedGarbageCollection: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - date: - type: string - format: date - garbage_type: - $ref: '#/components/schemas/GarbageTypeEnum' - PatchedManual: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - version_number: - type: integer - maximum: 2147483647 - minimum: 0 - file: - type: string - format: uri - nullable: true - PatchedPictureBuilding: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - picture: - type: string - format: uri - nullable: true - description: - type: string - nullable: true - timestamp: - type: string - format: date-time - type: - $ref: '#/components/schemas/TypeEnum' - PatchedRegion: - type: object - properties: - id: - type: integer - readOnly: true - region: - type: string - maxLength: 40 - PatchedRole: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 20 - rank: - type: integer - maximum: 2147483647 - minimum: 0 - description: - type: string - nullable: true - PatchedStudBuildTour: - type: object - properties: - id: - type: integer - readOnly: true - building_on_tour: - type: integer - nullable: true - date: - type: string - format: date - student: - type: integer - nullable: true - PatchedTour: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 40 - region: - type: integer - nullable: true - modified_at: - type: string - format: date-time - nullable: true - PatchedUser: - type: object - properties: - id: - type: integer - readOnly: true - is_active: - type: boolean - email: - type: string - format: email - readOnly: true - title: Email address - first_name: - type: string - maxLength: 40 - last_name: - type: string - maxLength: 40 - phone_number: - type: string - maxLength: 128 - region: - type: array - items: - type: integer - role: - type: integer - nullable: true - PictureBuilding: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - picture: - type: string - format: uri - nullable: true - description: - type: string - nullable: true - timestamp: - type: string - format: date-time - type: - $ref: '#/components/schemas/TypeEnum' - required: - - building - - id - - timestamp - - type - Region: - type: object - properties: - id: - type: integer - readOnly: true - region: - type: string - maxLength: 40 - required: - - id - - region - Register: - type: object - properties: - username: - type: string - maxLength: 0 - minLength: 1 - email: - type: string - format: email - password1: - type: string - writeOnly: true - password2: - type: string - writeOnly: true - required: - - email - - password1 - - password2 - ResendEmailVerification: - type: object - properties: - email: - type: string - format: email - required: - - email - RestAuthDetail: - type: object - properties: - detail: - type: string - readOnly: true - required: - - detail - Role: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 20 - rank: - type: integer - maximum: 2147483647 - minimum: 0 - description: - type: string - nullable: true - required: - - id - - name - - rank - StudBuildTour: - type: object - properties: - id: - type: integer - readOnly: true - building_on_tour: - type: integer - nullable: true - date: - type: string - format: date - student: - type: integer - nullable: true - required: - - date - - id - TokenRefresh: - type: object - properties: - access: - type: string - readOnly: true - refresh: - type: string - required: - - access - - refresh - TokenVerify: - type: object - properties: - token: - type: string - writeOnly: true - required: - - token - Tour: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 40 - region: - type: integer - nullable: true - modified_at: - type: string - format: date-time - nullable: true - required: - - id - - name - TypeEnum: - enum: - - AA - - BI - - VE - - OP - type: string - description: |- - * `AA` - Aankomst - * `BI` - Binnen - * `VE` - Vertrek - * `OP` - Opmerking - User: - type: object - properties: - id: - type: integer - readOnly: true - is_active: - type: boolean - email: - type: string - format: email - readOnly: true - title: Email address - first_name: - type: string - maxLength: 40 - last_name: - type: string - maxLength: 40 - phone_number: - type: string - maxLength: 128 - region: - type: array - items: - type: integer - role: - type: integer - nullable: true - required: - - email - - first_name - - id - - last_name - - phone_number - - region - VerifyEmail: - type: object - properties: - key: - type: string - writeOnly: true - required: - - key - securitySchemes: - jwtCookieAuth: - type: apiKey - in: cookie - name: jwt-auth - jwtHeaderAuth: - type: http - scheme: bearer - bearerFormat: JWT From e66bca13cb22297692339134c3e42b23501189ce Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 18:44:22 +0100 Subject: [PATCH 0225/1000] #106 added api-docs.md --- backend/schema.yml | 2756 ----------------------------------- docs/wiki-pages/api-docs.md | 74 + 2 files changed, 74 insertions(+), 2756 deletions(-) delete mode 100644 backend/schema.yml create mode 100644 docs/wiki-pages/api-docs.md diff --git a/backend/schema.yml b/backend/schema.yml deleted file mode 100644 index 9888683d..00000000 --- a/backend/schema.yml +++ /dev/null @@ -1,2756 +0,0 @@ -openapi: 3.0.3 -info: - title: Dr-Trottoir API - version: 1.0.0 - description: This is the documentation for the Dr-Trottoir API -paths: - /: - get: - operationId: root_retrieve - description: If you are logged in, you should see "Hello from the DrTrottoir - API!" - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - description: No response body - '400': - description: No response body - '403': - description: No response body - '401': - description: No response body - /authentication/account-confirm-email/: - post: - operationId: authentication_account_confirm_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/account-confirm-email/{key}/: - post: - operationId: authentication_account_confirm_email_create_2 - parameters: - - in: path - name: key - schema: - type: string - required: true - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/login/: - post: - operationId: authentication_login_create - description: |- - Check the credentials and return the REST Token - if the credentials are valid and authenticated. - Calls Django Auth login method to register User ID - in Django session framework - - Accept the following POST parameters: username, password - Return the REST Framework Token Object's key. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Login' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Login' - multipart/form-data: - schema: - $ref: '#/components/schemas/Login' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Login' - description: '' - /authentication/logout/: - get: - operationId: authentication_logout_retrieve - description: |- - Calls Django logout method and delete the Token object - assigned to the current User object. - - Accepts/Returns nothing. - tags: - - authentication - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - description: '' - post: - operationId: authentication_logout_create - description: |- - Calls Django logout method and delete the Token object - assigned to the current User object. - - Accepts/Returns nothing. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TokenRefresh' - multipart/form-data: - schema: - $ref: '#/components/schemas/TokenRefresh' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - description: '' - /authentication/password/change/: - post: - operationId: authentication_password_change_create - description: |- - Calls Django Auth SetPasswordForm save method. - - Accepts the following POST parameters: new_password1, new_password2 - Returns the success/fail message. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PasswordChange' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PasswordChange' - multipart/form-data: - schema: - $ref: '#/components/schemas/PasswordChange' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/password/reset/: - post: - operationId: authentication_password_reset_create - description: |- - Calls Django Auth PasswordResetForm save method. - - Accepts the following POST parameters: email - Returns the success/fail message. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PasswordReset' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PasswordReset' - multipart/form-data: - schema: - $ref: '#/components/schemas/PasswordReset' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/password/reset/confirm/{uidb64}/{token}/: - post: - operationId: authentication_password_reset_confirm_create - description: |- - Password reset e-mail link is confirmed, therefore - this resets the user's password. - - Accepts the following POST parameters: token, uid, - new_password1, new_password2 - Returns the success/fail message. - parameters: - - in: path - name: token - schema: - type: string - required: true - - in: path - name: uidb64 - schema: - type: string - required: true - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PasswordResetConfirm' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PasswordResetConfirm' - multipart/form-data: - schema: - $ref: '#/components/schemas/PasswordResetConfirm' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/signup/: - post: - operationId: authentication_signup_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Register' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Register' - multipart/form-data: - schema: - $ref: '#/components/schemas/Register' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/JWT' - description: '' - /authentication/signup/resend-email/: - post: - operationId: authentication_signup_resend_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ResendEmailVerification' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/ResendEmailVerification' - multipart/form-data: - schema: - $ref: '#/components/schemas/ResendEmailVerification' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/signup/verify-email/: - post: - operationId: authentication_signup_verify_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /authentication/token/refresh/: - post: - operationId: authentication_token_refresh_create - description: |- - Takes a refresh type JSON web token and returns an access type JSON web - token if the refresh token is valid. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TokenRefresh' - multipart/form-data: - schema: - $ref: '#/components/schemas/TokenRefresh' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenRefresh' - description: '' - /authentication/token/verify/: - post: - operationId: authentication_token_verify_create - description: |- - Takes a token and indicates if it is valid. This view provides no - information about a token's fitness for a particular use. - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TokenVerify' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TokenVerify' - multipart/form-data: - schema: - $ref: '#/components/schemas/TokenVerify' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/TokenVerify' - description: '' - /authentication/verify-email/: - post: - operationId: authentication_verify_email_create - tags: - - authentication - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyEmail' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/VerifyEmail' - multipart/form-data: - schema: - $ref: '#/components/schemas/VerifyEmail' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/RestAuthDetail' - description: '' - /building/: - post: - operationId: building_create - description: Create a new building - tags: - - building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Building' - multipart/form-data: - schema: - $ref: '#/components/schemas/Building' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - /building/{building_id}/: - get: - operationId: building_retrieve - description: Get info about building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - patch: - operationId: building_partial_update - description: Edit building with given ID - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuilding' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuilding' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuilding' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - delete: - operationId: building_destroy - description: Delete building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /building/all/: - get: - operationId: building_all_retrieve - description: Get all buildings - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - /building/owner/{owner_id}/: - get: - operationId: building_owner_retrieve - description: Get all buildings owned by syndic with given id - parameters: - - in: path - name: owner_id - schema: - type: integer - required: true - tags: - - building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Building' - description: '' - '400': - description: No response body - /building_comment/: - post: - operationId: building_comment_create - description: Create a new BuildingComment - tags: - - building_comment - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/BuildingComment' - multipart/form-data: - schema: - $ref: '#/components/schemas/BuildingComment' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - /building_comment/{building_comment_id}/: - get: - operationId: building_comment_retrieve - description: Get an invividual BuildingComment with given id - parameters: - - in: path - name: building_comment_id - schema: - type: integer - required: true - tags: - - building_comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - patch: - operationId: building_comment_partial_update - description: Edit BuildingComment with given id - parameters: - - in: path - name: building_comment_id - schema: - type: integer - required: true - tags: - - building_comment - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuildingComment' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuildingComment' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuildingComment' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - delete: - operationId: building_comment_destroy - description: Delete a BuildingComment with given id - parameters: - - in: path - name: building_comment_id - schema: - type: integer - required: true - tags: - - building_comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /building_comment/all/: - get: - operationId: building_comment_all_retrieve - description: Get all BuildingComments in database - tags: - - building_comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - /building_comment/building/{building_id}/: - get: - operationId: building_comment_building_retrieve - description: Get all BuildingComments of building with given building id - parameters: - - in: path - name: building_id - schema: - type: string - required: true - tags: - - building_comment - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingComment' - description: '' - '400': - description: No response body - /building_on_tour/: - post: - operationId: building_on_tour_create - description: Create a new BuildingOnTour with data from post - tags: - - building_on_tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/BuildingTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/BuildingTour' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - '400': - description: No response body - /building_on_tour/{building_tour_id}/: - get: - operationId: building_on_tour_retrieve - description: Get info about a BuildingOnTour with given id - parameters: - - in: path - name: building_tour_id - schema: - type: integer - required: true - tags: - - building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - '400': - description: No response body - patch: - operationId: building_on_tour_partial_update - description: edit info about a BuildingOnTour with given id - parameters: - - in: path - name: building_tour_id - schema: - type: integer - required: true - tags: - - building_on_tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuildingTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuildingTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuildingTour' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - '400': - description: No response body - delete: - operationId: building_on_tour_destroy - description: delete a BuildingOnTour from the database - parameters: - - in: path - name: building_tour_id - schema: - type: integer - required: true - tags: - - building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /building_on_tour/all/: - get: - operationId: building_on_tour_all_retrieve - description: Get all buildings on tours - tags: - - building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingTour' - description: '' - /buildingurl/: - post: - operationId: buildingurl_create - description: Create a new building url - tags: - - buildingurl - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/BuildingUrl' - multipart/form-data: - schema: - $ref: '#/components/schemas/BuildingUrl' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - description: '' - '400': - description: No response body - /buildingurl/{building_url_id}/: - get: - operationId: buildingurl_retrieve - description: Get info about a buildingurl with given id - parameters: - - in: path - name: building_url_id - schema: - type: integer - required: true - tags: - - buildingurl - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - description: '' - '400': - description: No response body - patch: - operationId: buildingurl_partial_update - description: Edit info about buildingurl with given id - parameters: - - in: path - name: building_url_id - schema: - type: integer - required: true - tags: - - buildingurl - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedBuildingUrl' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedBuildingUrl' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedBuildingUrl' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - description: '' - '400': - description: No response body - delete: - operationId: buildingurl_destroy - description: Delete buildingurl with given id - parameters: - - in: path - name: building_url_id - schema: - type: integer - required: true - tags: - - buildingurl - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /buildingurl/all/: - get: - operationId: buildingurl_all_retrieve - description: Get all building urls - tags: - - buildingurl - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - description: '' - /buildingurl/building/{building_id}/: - get: - operationId: buildingurl_building_retrieve - description: Get all building urls of a given building - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - buildingurl - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - description: '' - /buildingurl/syndic/{syndic_id}/: - get: - operationId: buildingurl_syndic_retrieve - description: Get all building urls of buildings where the user with given user - id is syndic - parameters: - - in: path - name: syndic_id - schema: - type: integer - required: true - tags: - - buildingurl - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BuildingUrl' - description: '' - /garbage_collection/: - post: - operationId: garbage_collection_create - description: Create new garbage collection - tags: - - garbage_collection - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/GarbageCollection' - multipart/form-data: - schema: - $ref: '#/components/schemas/GarbageCollection' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - '400': - description: No response body - /garbage_collection/{garbage_collection_id}/: - get: - operationId: garbage_collection_retrieve - description: Get info about a garbage collection with given id - parameters: - - in: path - name: garbage_collection_id - schema: - type: string - required: true - tags: - - garbage_collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - '400': - description: No response body - patch: - operationId: garbage_collection_partial_update - description: Edit garbage collection with given id - parameters: - - in: path - name: garbage_collection_id - schema: - type: string - required: true - tags: - - garbage_collection - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedGarbageCollection' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedGarbageCollection' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedGarbageCollection' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - '400': - description: No response body - delete: - operationId: garbage_collection_destroy - description: Delete garbage collection with given id - parameters: - - in: path - name: garbage_collection_id - schema: - type: string - required: true - tags: - - garbage_collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /garbage_collection/all/: - get: - operationId: garbage_collection_all_retrieve - description: Get all garbage collections - tags: - - garbage_collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - /garbage_collection/building/{building_id}/: - get: - operationId: garbage_collection_building_retrieve - description: Get info about all garbage collections of a building with given - id - parameters: - - in: path - name: building_id - schema: - type: string - required: true - tags: - - garbage_collection - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GarbageCollection' - description: '' - /manual/: - post: - operationId: manual_create - description: Create a new manual with data from post - tags: - - manual - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Manual' - multipart/form-data: - schema: - $ref: '#/components/schemas/Manual' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - /manual/{manual_id}/: - get: - operationId: manual_retrieve - description: Get info about a manual with given id - parameters: - - in: path - name: manual_id - schema: - type: integer - required: true - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - patch: - operationId: manual_partial_update - description: Edit info about a manual with given id - parameters: - - in: path - name: manual_id - schema: - type: integer - required: true - tags: - - manual - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedManual' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedManual' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedManual' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - delete: - operationId: manual_destroy - description: Delete manual with given id - parameters: - - in: path - name: manual_id - schema: - type: integer - required: true - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /manual/all/: - get: - operationId: manual_all_retrieve - description: Get all manuals - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - /manual/building/{building_id}/: - get: - operationId: manual_building_retrieve - description: Get all manuals of a building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - manual - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Manual' - description: '' - '400': - description: No response body - /picture_building/: - post: - operationId: picture_building_create - description: Create a new PictureBuilding - tags: - - picture_building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PictureBuilding' - multipart/form-data: - schema: - $ref: '#/components/schemas/PictureBuilding' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - '400': - description: No response body - /picture_building/{picture_building_id}/: - get: - operationId: picture_building_retrieve - description: Get PictureBuilding with given id - parameters: - - in: path - name: picture_building_id - schema: - type: integer - required: true - tags: - - picture_building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - '400': - description: No response body - patch: - operationId: picture_building_partial_update - description: Edit info about PictureBuilding with given id - parameters: - - in: path - name: picture_building_id - schema: - type: integer - required: true - tags: - - picture_building - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedPictureBuilding' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedPictureBuilding' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedPictureBuilding' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - '400': - description: No response body - delete: - operationId: picture_building_destroy - description: delete a pictureBuilding from the database - parameters: - - in: path - name: picture_building_id - schema: - type: integer - required: true - tags: - - picture_building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /picture_building/all/: - get: - operationId: picture_building_all_retrieve - description: Get all pictureBuilding - tags: - - picture_building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - /picture_building/building/{building_id}/: - get: - operationId: picture_building_building_retrieve - description: Get all pictures of a building with given id - parameters: - - in: path - name: building_id - schema: - type: integer - required: true - tags: - - picture_building - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PictureBuilding' - description: '' - /region/: - post: - operationId: region_create - description: Create a new region - tags: - - region - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Region' - multipart/form-data: - schema: - $ref: '#/components/schemas/Region' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - '400': - description: No response body - /region/{region_id}/: - get: - operationId: region_retrieve - description: Get info about a Region with given id - parameters: - - in: path - name: region_id - schema: - type: integer - required: true - tags: - - region - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - '400': - description: No response body - patch: - operationId: region_partial_update - description: Edit Region with given id - parameters: - - in: path - name: region_id - schema: - type: integer - required: true - tags: - - region - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedRegion' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedRegion' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedRegion' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - '400': - description: No response body - delete: - operationId: region_destroy - description: delete a region with given id - parameters: - - in: path - name: region_id - schema: - type: integer - required: true - tags: - - region - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /region/all/: - get: - operationId: region_all_retrieve - description: Get all regions - tags: - - region - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Region' - description: '' - /role/: - post: - operationId: role_create - description: Create a new role - tags: - - role - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Role' - multipart/form-data: - schema: - $ref: '#/components/schemas/Role' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - '400': - description: No response body - /role/{role_id}/: - get: - operationId: role_retrieve - description: Get info about a Role with given id - parameters: - - in: path - name: role_id - schema: - type: integer - required: true - tags: - - role - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - '400': - description: No response body - patch: - operationId: role_partial_update - description: Edit info about a Role with given id - parameters: - - in: path - name: role_id - schema: - type: integer - required: true - tags: - - role - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedRole' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedRole' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedRole' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - '400': - description: No response body - delete: - operationId: role_destroy - description: Delete a Role with given id - parameters: - - in: path - name: role_id - schema: - type: integer - required: true - tags: - - role - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /role/all/: - get: - operationId: role_all_retrieve - description: Get all roles - tags: - - role - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Role' - description: '' - /student_at_building_on_tour/: - post: - operationId: student_at_building_on_tour_create - description: Create a new StudentAtBuildingOnTour - tags: - - student_at_building_on_tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/StudBuildTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/StudBuildTour' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - '400': - description: No response body - /student_at_building_on_tour/{student_at_building_on_tour_id}/: - get: - operationId: student_at_building_on_tour_retrieve - description: Get an individual StudentAtBuildingOnTour with given id - parameters: - - in: path - name: student_at_building_on_tour_id - schema: - type: integer - required: true - tags: - - student_at_building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - '400': - description: No response body - patch: - operationId: student_at_building_on_tour_partial_update - description: Edit info about an individual StudentAtBuildingOnTour with given - id - parameters: - - in: path - name: student_at_building_on_tour_id - schema: - type: integer - required: true - tags: - - student_at_building_on_tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedStudBuildTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedStudBuildTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedStudBuildTour' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - '400': - description: No response body - delete: - operationId: student_at_building_on_tour_destroy - description: Delete StudentAtBuildingOnTour with given id - parameters: - - in: path - name: student_at_building_on_tour_id - schema: - type: integer - required: true - tags: - - student_at_building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /student_at_building_on_tour/all/: - get: - operationId: student_at_building_on_tour_all_retrieve - description: Get all StudentAtBuildingOnTours - tags: - - student_at_building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - /student_at_building_on_tour/student/{student_id}/: - get: - operationId: student_at_building_on_tour_student_retrieve - description: Get all StudentAtBuildingOnTour for a student with given id - parameters: - - in: path - name: student_id - schema: - type: integer - required: true - tags: - - student_at_building_on_tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/StudBuildTour' - description: '' - /tour/: - post: - operationId: tour_create - description: Create a new tour - tags: - - tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Tour' - multipart/form-data: - schema: - $ref: '#/components/schemas/Tour' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - '400': - description: No response body - /tour/{tour_id}/: - get: - operationId: tour_retrieve - description: Get info about a Tour with given id - parameters: - - in: path - name: tour_id - schema: - type: integer - required: true - tags: - - tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - '400': - description: No response body - patch: - operationId: tour_partial_update - description: Edit a tour with given id - parameters: - - in: path - name: tour_id - schema: - type: integer - required: true - tags: - - tour - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedTour' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedTour' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedTour' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - '400': - description: No response body - delete: - operationId: tour_destroy - description: Delete a tour with given id - parameters: - - in: path - name: tour_id - schema: - type: integer - required: true - tags: - - tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /tour/all/: - get: - operationId: tour_all_retrieve - description: Get all tours - tags: - - tour - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/Tour' - description: '' - /user/: - post: - operationId: user_create - description: Create a new user - tags: - - user - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/User' - multipart/form-data: - schema: - $ref: '#/components/schemas/User' - required: true - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' - '400': - description: No response body - /user/{user_id}/: - get: - operationId: user_retrieve - description: Get info about user with given id - parameters: - - in: path - name: user_id - schema: - type: integer - required: true - tags: - - user - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' - '400': - description: No response body - patch: - operationId: user_partial_update - description: Edit user with given id - parameters: - - in: path - name: user_id - schema: - type: integer - required: true - tags: - - user - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PatchedUser' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/PatchedUser' - multipart/form-data: - schema: - $ref: '#/components/schemas/PatchedUser' - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' - '400': - description: No response body - delete: - operationId: user_destroy - description: |- - Delete user with given id - We don't acutally delete a user, we put the user on inactive mode - parameters: - - in: path - name: user_id - schema: - type: integer - required: true - tags: - - user - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '204': - description: No response body - '400': - description: No response body - /user/all/: - get: - operationId: user_all_retrieve - description: Get all users - tags: - - user - security: - - jwtHeaderAuth: [] - - jwtCookieAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/User' - description: '' -components: - schemas: - Building: - type: object - properties: - id: - type: integer - readOnly: true - city: - type: string - maxLength: 40 - postal_code: - type: string - maxLength: 10 - street: - type: string - maxLength: 60 - house_number: - type: string - maxLength: 10 - client_number: - type: string - nullable: true - maxLength: 40 - duration: - type: string - format: time - syndic: - type: integer - nullable: true - region: - type: integer - nullable: true - name: - type: string - nullable: true - maxLength: 100 - required: - - city - - house_number - - id - - postal_code - - street - BuildingComment: - type: object - properties: - id: - type: integer - readOnly: true - comment: - type: string - date: - type: string - format: date-time - building: - type: integer - required: - - building - - comment - - date - - id - BuildingTour: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - tour: - type: integer - index: - type: integer - maximum: 2147483647 - minimum: 0 - required: - - building - - id - - index - - tour - BuildingUrl: - type: object - properties: - id: - type: integer - readOnly: true - first_name_resident: - type: string - maxLength: 40 - last_name_resident: - type: string - maxLength: 40 - building: - type: integer - required: - - building - - first_name_resident - - id - - last_name_resident - GarbageCollection: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - date: - type: string - format: date - garbage_type: - $ref: '#/components/schemas/GarbageTypeEnum' - required: - - building - - date - - garbage_type - - id - GarbageTypeEnum: - enum: - - GFT - - GLS - - GRF - - KER - - PAP - - PMD - - RES - type: string - description: |- - * `GFT` - GFT - * `GLS` - Glas - * `GRF` - Grof vuil - * `KER` - Kerstbomen - * `PAP` - Papier - * `PMD` - PMD - * `RES` - Restafval - JWT: - type: object - description: Serializer for JWT authentication. - properties: - access_token: - type: string - refresh_token: - type: string - user: - $ref: '#/components/schemas/User' - required: - - access_token - - refresh_token - - user - Login: - type: object - properties: - username: - type: string - email: - type: string - format: email - password: - type: string - required: - - password - Manual: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - version_number: - type: integer - maximum: 2147483647 - minimum: 0 - file: - type: string - format: uri - nullable: true - required: - - building - - id - PasswordChange: - type: object - properties: - new_password1: - type: string - maxLength: 128 - new_password2: - type: string - maxLength: 128 - required: - - new_password1 - - new_password2 - PasswordReset: - type: object - description: Serializer for requesting a password reset e-mail. - properties: - email: - type: string - format: email - required: - - email - PasswordResetConfirm: - type: object - description: Serializer for confirming a password reset attempt. - properties: - new_password1: - type: string - maxLength: 128 - new_password2: - type: string - maxLength: 128 - uid: - type: string - token: - type: string - required: - - new_password1 - - new_password2 - - token - - uid - PatchedBuilding: - type: object - properties: - id: - type: integer - readOnly: true - city: - type: string - maxLength: 40 - postal_code: - type: string - maxLength: 10 - street: - type: string - maxLength: 60 - house_number: - type: string - maxLength: 10 - client_number: - type: string - nullable: true - maxLength: 40 - duration: - type: string - format: time - syndic: - type: integer - nullable: true - region: - type: integer - nullable: true - name: - type: string - nullable: true - maxLength: 100 - PatchedBuildingComment: - type: object - properties: - id: - type: integer - readOnly: true - comment: - type: string - date: - type: string - format: date-time - building: - type: integer - PatchedBuildingTour: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - tour: - type: integer - index: - type: integer - maximum: 2147483647 - minimum: 0 - PatchedBuildingUrl: - type: object - properties: - id: - type: integer - readOnly: true - first_name_resident: - type: string - maxLength: 40 - last_name_resident: - type: string - maxLength: 40 - building: - type: integer - PatchedGarbageCollection: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - date: - type: string - format: date - garbage_type: - $ref: '#/components/schemas/GarbageTypeEnum' - PatchedManual: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - version_number: - type: integer - maximum: 2147483647 - minimum: 0 - file: - type: string - format: uri - nullable: true - PatchedPictureBuilding: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - picture: - type: string - format: uri - nullable: true - description: - type: string - nullable: true - timestamp: - type: string - format: date-time - type: - $ref: '#/components/schemas/TypeEnum' - PatchedRegion: - type: object - properties: - id: - type: integer - readOnly: true - region: - type: string - maxLength: 40 - PatchedRole: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 20 - rank: - type: integer - maximum: 2147483647 - minimum: 0 - description: - type: string - nullable: true - PatchedStudBuildTour: - type: object - properties: - id: - type: integer - readOnly: true - building_on_tour: - type: integer - nullable: true - date: - type: string - format: date - student: - type: integer - nullable: true - PatchedTour: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 40 - region: - type: integer - nullable: true - modified_at: - type: string - format: date-time - nullable: true - PatchedUser: - type: object - properties: - id: - type: integer - readOnly: true - is_active: - type: boolean - email: - type: string - format: email - readOnly: true - title: Email address - first_name: - type: string - maxLength: 40 - last_name: - type: string - maxLength: 40 - phone_number: - type: string - maxLength: 128 - region: - type: array - items: - type: integer - role: - type: integer - nullable: true - PictureBuilding: - type: object - properties: - id: - type: integer - readOnly: true - building: - type: integer - picture: - type: string - format: uri - nullable: true - description: - type: string - nullable: true - timestamp: - type: string - format: date-time - type: - $ref: '#/components/schemas/TypeEnum' - required: - - building - - id - - timestamp - - type - Region: - type: object - properties: - id: - type: integer - readOnly: true - region: - type: string - maxLength: 40 - required: - - id - - region - Register: - type: object - properties: - username: - type: string - maxLength: 0 - minLength: 1 - email: - type: string - format: email - password1: - type: string - writeOnly: true - password2: - type: string - writeOnly: true - required: - - email - - password1 - - password2 - ResendEmailVerification: - type: object - properties: - email: - type: string - format: email - required: - - email - RestAuthDetail: - type: object - properties: - detail: - type: string - readOnly: true - required: - - detail - Role: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 20 - rank: - type: integer - maximum: 2147483647 - minimum: 0 - description: - type: string - nullable: true - required: - - id - - name - - rank - StudBuildTour: - type: object - properties: - id: - type: integer - readOnly: true - building_on_tour: - type: integer - nullable: true - date: - type: string - format: date - student: - type: integer - nullable: true - required: - - date - - id - TokenRefresh: - type: object - properties: - access: - type: string - readOnly: true - refresh: - type: string - required: - - access - - refresh - TokenVerify: - type: object - properties: - token: - type: string - writeOnly: true - required: - - token - Tour: - type: object - properties: - id: - type: integer - readOnly: true - name: - type: string - maxLength: 40 - region: - type: integer - nullable: true - modified_at: - type: string - format: date-time - nullable: true - required: - - id - - name - TypeEnum: - enum: - - AA - - BI - - VE - - OP - type: string - description: |- - * `AA` - Aankomst - * `BI` - Binnen - * `VE` - Vertrek - * `OP` - Opmerking - User: - type: object - properties: - id: - type: integer - readOnly: true - is_active: - type: boolean - email: - type: string - format: email - readOnly: true - title: Email address - first_name: - type: string - maxLength: 40 - last_name: - type: string - maxLength: 40 - phone_number: - type: string - maxLength: 128 - region: - type: array - items: - type: integer - role: - type: integer - nullable: true - required: - - email - - first_name - - id - - last_name - - phone_number - - region - VerifyEmail: - type: object - properties: - key: - type: string - writeOnly: true - required: - - key - securitySchemes: - jwtCookieAuth: - type: apiKey - in: cookie - name: jwt-auth - jwtHeaderAuth: - type: http - scheme: bearer - bearerFormat: JWT diff --git a/docs/wiki-pages/api-docs.md b/docs/wiki-pages/api-docs.md new file mode 100644 index 00000000..07aa0c96 --- /dev/null +++ b/docs/wiki-pages/api-docs.md @@ -0,0 +1,74 @@ +## Introduction +We used [drf-spectacular](https://github.com/tfranzel/drf-spectacular#customization-by-using-extend_schema) to automatically +generate [OpenAPI 3.0/Swagger](https://spec.openapis.org/oas/v3.0.3) compliant documentation for our API. Drf-spectacular +will automatically update our documentation whenever something changes, so there is no need to run any extra commands or code. +This makes it a lot easier to maintain our documentation. To access it, head over to http://localhost/api/docs/ui. + +While it already generates a lot out of the box, it will still be necessary to add/edit some things ourselves. +This will be covered in the next section. + +## How to expand and edit the schema +The [@extend_schema](https://drf-spectacular.readthedocs.io/en/latest/drf_spectacular.html#drf_spectacular.utils.extend_schema) +decorator from [drf-spectacular](https://github.com/tfranzel/drf-spectacular#customization-by-using-extend_schema) will cover most if not all use cases for customizing the documentation. Below is an example +of how to use it: + +```python +from drf_spectacular.utils import extend_schema + +class AllUsersView(APIView): + serializer_class = UserSerializer + + @extend_schema( + parameters=[OpenApiParameter(name='test', description='test parameter', required=False, type=str)], + description="test description", + responses={200: UserSerializer, + 400: None} + ) + def get(self, request): + """ + Get all users + """ + + user_instances = User.objects.all() + + if not user_instances: + return Response( + {"res": "No users found"}, + status=status.HTTP_400_BAD_REQUEST + ) + + serializer = UserSerializer(user_instances, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) +``` + +Before adding [@extend_schema](https://drf-spectacular.readthedocs.io/en/latest/drf_spectacular.html#drf_spectacular.utils.extend_schema), +the automatically generated documentation looks like this: +![user_before_extend](./img/user_before_extend.jpg) + +Since the request from the example is a `GET` request, automatically generating the 200 response code is not an issue, +however, it's still missing the 400 response code. To fix this, use [@extend_schema](https://drf-spectacular.readthedocs.io/en/latest/drf_spectacular.html#drf_spectacular.utils.extend_schema) +like in the example above. You'll have to define this right before any request function if you want to change or expand the +default documentation. The most common fields you'll need are also used in the example. Please note that even though the documentation +for the 200 response code has already been generated without [@extend_schema](https://drf-spectacular.readthedocs.io/en/latest/drf_spectacular.html#drf_spectacular.utils.extend_schema), +we'll still need to respecify it since using the `responses` field will override all old responses. The documentation for `/user/all` now looks like this: + +![user_after_extend](./img/user_after_extend.jpg) + +Now note how we set `serializer_class = UserSerializer` without ever using `serializer_class`. This is there to help drf-spectacular +parse our code, so don't forget to include this is in every view with the right serializer! If you want to make sure you did everything correctly, +check out the next section to find out how you can get error and warning outputs. + +For another (bigger) example, click [here](https://github.com/tfranzel/drf-spectacular#usage). For the full documentation of +[drf-spectacular](https://github.com/tfranzel/drf-spectacular#customization-by-using-extend_schema), click +[here](https://drf-spectacular.readthedocs.io/en/latest/). + +## The schema +It's also possible to generate a general `schema.yml` file which will contain the documentation in a format that can be +exported to other use cases. There are two ways to do this. The first one is just to head over to http://localhost/api/docs/. +This will automatically download the `schema.yml` file. The second one is to run the following command: +```bash +docker-compose exec backend python manage.py spectacular --file schema.yml +``` +This method can be useful because it will also produce errors and warnings when generating the file. This means that this +is a good way to doublecheck whether drf-spectacular was used correctly. + From 10af8f2f11d9ebb8da36d32b1f8573eb74379d84 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Thu, 23 Mar 2023 18:45:57 +0100 Subject: [PATCH 0226/1000] #106 updated api-docs.md --- docs/wiki-pages/api-docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wiki-pages/api-docs.md b/docs/wiki-pages/api-docs.md index 07aa0c96..e5776b59 100644 --- a/docs/wiki-pages/api-docs.md +++ b/docs/wiki-pages/api-docs.md @@ -2,7 +2,7 @@ We used [drf-spectacular](https://github.com/tfranzel/drf-spectacular#customization-by-using-extend_schema) to automatically generate [OpenAPI 3.0/Swagger](https://spec.openapis.org/oas/v3.0.3) compliant documentation for our API. Drf-spectacular will automatically update our documentation whenever something changes, so there is no need to run any extra commands or code. -This makes it a lot easier to maintain our documentation. To access it, head over to http://localhost/api/docs/ui. +This makes it a lot easier to maintain our documentation. To access our documentation, head over to http://localhost/api/docs/ui. While it already generates a lot out of the box, it will still be necessary to add/edit some things ourselves. This will be covered in the next section. From b88fa1e77eaaeca94cf7affcb28b5d3ca2117033 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 00:23:49 +0100 Subject: [PATCH 0227/1000] 'from' and 'to' keys supported in request body GET for PictureBuilding (#57) --- backend/picture_building/views.py | 55 ++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 83636b04..752846fd 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -1,3 +1,4 @@ +from django.db.models import QuerySet from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView @@ -6,10 +7,41 @@ from base.serializers import PictureBuildingSerializer from util.request_response_util import * from drf_spectacular.utils import extend_schema +from datetime import datetime + +DESCRIPTION = "Optionally, you can filter by date, by using the keys \"from\" and/or \"to\". When filtering, \"from\" and \"to\" are included in the result. The keys must be in format \"%Y-%m-%d %H:%M:%S\" or \"%Y-%m-%d\"." TRANSLATE = {"building": "building_id"} +def get_pictures_in_range(instances, from_date: str = None, to_date: str = None) -> Response | QuerySet: + from_d = None + to_d = None + if from_date: + try: + from_d = datetime.strptime(from_date, '%Y-%m-%d %H:%M:%S') + except ValueError: + try: + from_d = datetime.strptime(from_date, '%Y-%m-%d') + except ValueError: + return Response(status=status.HTTP_400_BAD_REQUEST, data="Invalid date format") + if to_date: + try: + to_d = datetime.strptime(to_date, '%Y-%m-%d %H:%M:%S') + except ValueError: + try: + to_d = datetime.strptime(to_date, '%Y-%m-%d') + except ValueError: + return Response(status=status.HTTP_400_BAD_REQUEST, data="Invalid date format") + + if from_d: + instances = instances.filter(timestamp__gte=from_d) + if to_date: + instances = instances.filter(timestamp__lte=to_d) + + return instances + + class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] serializer_class = PictureBuildingSerializer @@ -92,19 +124,26 @@ class PicturesOfBuildingView(APIView): serializer_class = PictureBuildingSerializer + @extend_schema(description=DESCRIPTION, responses={200: PictureBuildingSerializer, 400: None}) def get(self, request, building_id): """ Get all pictures of a building with given id """ - building_instance = Building.objects.filter(building_id=building_id) + building_instance = Building.objects.filter(id=building_id) if not building_instance: return bad_request(building_instance) building_instance = building_instance[0] self.check_object_permissions(request, building_instance) - picture_building_instances = PictureBuilding.objects.filter(building_id=building_id) - serializer = PictureBuildingSerializer(picture_building_instances, many=True) + picture_building_instances = PictureBuilding.objects.filter(building=building_id) + res = get_pictures_in_range(picture_building_instances, from_date=request.data.get("from"), + to_date=request.data.get("to")) + + if isinstance(res, Response): + return res + + serializer = PictureBuildingSerializer(res, many=True) return get_success(serializer) @@ -112,11 +151,19 @@ class AllPictureBuildingsView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = PictureBuildingSerializer + @extend_schema(description=DESCRIPTION) def get(self, request): """ Get all pictureBuilding """ picture_building_instances = PictureBuilding.objects.all() - serializer = PictureBuildingSerializer(picture_building_instances, many=True) + data = request_to_dict(request.data) + + res = get_pictures_in_range(picture_building_instances, from_date=data.get("from"), to_date=data.get("to")) + + if isinstance(res, Response): + return res + + serializer = PictureBuildingSerializer(res, many=True) return get_success(serializer) From d0b613cc0eb2dd43941617e44ff19e35bf10142a Mon Sep 17 00:00:00 2001 From: TiboStr Date: Thu, 23 Mar 2023 23:30:35 +0000 Subject: [PATCH 0228/1000] Auto formatted code --- backend/picture_building/views.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 752846fd..a0ea0faf 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -9,7 +9,7 @@ from drf_spectacular.utils import extend_schema from datetime import datetime -DESCRIPTION = "Optionally, you can filter by date, by using the keys \"from\" and/or \"to\". When filtering, \"from\" and \"to\" are included in the result. The keys must be in format \"%Y-%m-%d %H:%M:%S\" or \"%Y-%m-%d\"." +DESCRIPTION = 'Optionally, you can filter by date, by using the keys "from" and/or "to". When filtering, "from" and "to" are included in the result. The keys must be in format "%Y-%m-%d %H:%M:%S" or "%Y-%m-%d".' TRANSLATE = {"building": "building_id"} @@ -19,18 +19,18 @@ def get_pictures_in_range(instances, from_date: str = None, to_date: str = None) to_d = None if from_date: try: - from_d = datetime.strptime(from_date, '%Y-%m-%d %H:%M:%S') + from_d = datetime.strptime(from_date, "%Y-%m-%d %H:%M:%S") except ValueError: try: - from_d = datetime.strptime(from_date, '%Y-%m-%d') + from_d = datetime.strptime(from_date, "%Y-%m-%d") except ValueError: return Response(status=status.HTTP_400_BAD_REQUEST, data="Invalid date format") if to_date: try: - to_d = datetime.strptime(to_date, '%Y-%m-%d %H:%M:%S') + to_d = datetime.strptime(to_date, "%Y-%m-%d %H:%M:%S") except ValueError: try: - to_d = datetime.strptime(to_date, '%Y-%m-%d') + to_d = datetime.strptime(to_date, "%Y-%m-%d") except ValueError: return Response(status=status.HTTP_400_BAD_REQUEST, data="Invalid date format") @@ -137,8 +137,9 @@ def get(self, request, building_id): self.check_object_permissions(request, building_instance) picture_building_instances = PictureBuilding.objects.filter(building=building_id) - res = get_pictures_in_range(picture_building_instances, from_date=request.data.get("from"), - to_date=request.data.get("to")) + res = get_pictures_in_range( + picture_building_instances, from_date=request.data.get("from"), to_date=request.data.get("to") + ) if isinstance(res, Response): return res From b264e6998dd622ec9ef0ff91d6c4613880ea3685 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 01:35:54 +0100 Subject: [PATCH 0229/1000] Responses in request_response_util ':' instead of ',' so it returns valid json --- backend/util/request_response_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index b2d62822..c8cdbecc 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -31,18 +31,18 @@ def set_keys_of_instance(instance, data: dict, translation: dict = {}): def bad_request(object_name="Object"): return Response( - {"res", f"{object_name} with given ID does not exist."}, + {"res": f"{object_name} with given ID does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) def not_found(object_name="Object"): - return Response({"res", f"{object_name} with given ID does not exists."}, status=status.HTTP_400_BAD_REQUEST) + return Response({"res": f"{object_name} with given ID does not exists."}, status=status.HTTP_400_BAD_REQUEST) def bad_request_relation(object1: str, object2: str): return Response( - {"res", f"There is no {object1} that is linked to {object2} with given id."}, + {"res": f"There is no {object1} that is linked to {object2} with given id."}, status=status.HTTP_400_BAD_REQUEST, ) From cc0eb6a115dfc7f2f42fe6d91d85375d7a63adc8 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Fri, 24 Mar 2023 01:37:00 +0100 Subject: [PATCH 0230/1000] CustomRegisterView + CustomRegisterSerializer (not yet working entirely as intended) --- backend/authentication/serializers.py | 21 +++++++---- backend/authentication/urls.py | 4 +-- backend/authentication/views.py | 52 +++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 3e0e1b5b..a8718015 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,12 +1,21 @@ from dj_rest_auth.registration.serializers import RegisterSerializer -from rest_framework import serializers +from phonenumber_field.serializerfields import PhoneNumberField +from rest_framework.serializers import CharField, IntegerField + +from base.models import User +from util.request_response_util import request_to_dict class CustomRegisterSerializer(RegisterSerializer): - first_name = serializers.CharField(required=True) - last_name = serializers.CharField(required=True) - verification_code = serializers.CharField(required=True) + first_name = CharField(required=True) + last_name = CharField(required=True) + phone_number = PhoneNumberField(required=True) + role = IntegerField(required=True) - def custom_signup(self, request, user): - pass + def custom_signup(self, request, user: User): + data = request_to_dict(request.data) + user.first_name = data['first_name'] + user.last_name = data['last_name'] + user.phone_number = data['phone_number'] + user.role_id = data['role'] diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 8d44fcfb..00634a3d 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -9,12 +9,12 @@ from authentication.views import ( LoginViewWithHiddenTokens, RefreshViewHiddenTokens, - LogoutViewWithBlacklisting, + LogoutViewWithBlacklisting, CustomRegisterView, ) urlpatterns = [ # URLs that do not require a session or valid token - path("signup/", include("dj_rest_auth.registration.urls")), + path("signup/", CustomRegisterView.as_view()), path("password/reset/", PasswordResetView.as_view()), path( "password/reset/confirm///", diff --git a/backend/authentication/views.py b/backend/authentication/views.py index c9d83a09..34454915 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -2,19 +2,67 @@ unset_jwt_cookies, CookieTokenRefreshSerializer, set_jwt_access_cookie, - set_jwt_refresh_cookie, + set_jwt_refresh_cookie, set_jwt_cookies, ) +from dj_rest_auth.utils import jwt_encode from dj_rest_auth.views import LogoutView, LoginView from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework_simplejwt.exceptions import TokenError from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.views import TokenRefreshView -from drf_spectacular.utils import extend_schema +from authentication.serializers import CustomRegisterSerializer +from base.models import Lobby from config import settings +from util.request_response_util import request_to_dict + + +class CustomRegisterView(APIView): + serializer_class = CustomRegisterSerializer + + def post(self, request): + """ + Register a new user + """ + data = request_to_dict(request.data) + + # check if there is a lobby entry for this email address + lobby_instances = Lobby.objects.filter(email=data.get('email')) + if not lobby_instances: + return Response( + {"res": f"There is no entry in the lobby for email address: {data.get('email')}"}, + status=status.HTTP_403_FORBIDDEN + ) + lobby_instance = lobby_instances[0] + # check if the verification code is valid + if lobby_instance.verification_code != data.get('verification_code'): + return Response( + {"res": "Invalid verification code"}, + status=status.HTTP_403_FORBIDDEN + ) + + request.data._mutable = True + request.data['role'] = lobby_instance.role_id + request.data._mutable = False + + # create a user + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.save(request) + # create an access and refresh token + access_token, refresh_token = jwt_encode(user) + + # add the user data to the response + response = Response({"res": str(user)}, status=status.HTTP_200_OK) + # set the cookie headers + set_jwt_cookies(response, access_token, access_token) + + return response class LogoutViewWithBlacklisting(LogoutView): From c4de043eedb0217e610e884f597130c186f23675 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 12:15:39 +0100 Subject: [PATCH 0231/1000] Added error messages for signup & login --- frontend/lib/login.tsx | 10 +---- frontend/pages/login.tsx | 53 ++++++++++++++++------ frontend/pages/signup.tsx | 93 ++++++++++++++++++++++++++++++++------- 3 files changed, 118 insertions(+), 38 deletions(-) diff --git a/frontend/lib/login.tsx b/frontend/lib/login.tsx index 45c01d1b..08e08d2d 100644 --- a/frontend/lib/login.tsx +++ b/frontend/lib/login.tsx @@ -19,15 +19,9 @@ export const login = (email: string, password: string): Promise> => { // TODO: This is a temporary request to endpoint token/refresh, change this endpoint once token/verify/ // or another endpoint is correctly set up - const verify_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}${process.env.NEXT_PUBLIC_API_REFRESH_TOKEN}`; + const verify_url: string = `${process.env.NEXT_PUBLIC_BASE_API_URL}`; - return api.post( - verify_url, - {}, - { - headers: { "Content-Type": "application/json" }, - } - ); + return api.get(verify_url); }; export default login; diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index f5403d24..fba33802 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -2,30 +2,32 @@ import BaseHeader from "@/components/header/BaseHeader"; import styles from "styles/Login.module.css"; import Image from "next/image"; import filler_image from "../public/filler_image.png"; -import Link from "next/link"; -import { login, verifyToken } from "@/lib/login"; -import { FormEvent, useEffect, useState } from "react"; -import { useRouter } from "next/router"; +import {login, verifyToken} from "@/lib/login"; +import React, {FormEvent, useEffect, useState} from "react"; +import {useRouter} from "next/router"; export default function Login() { const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [errorMessages, setErrorMessages] = useState( []); // try and log in to the application using existing refresh token useEffect(() => { verifyToken().then( - async (res) => { - console.log(res); + async () => { await router.push("/welcome"); }, (err) => { - console.error("Error: token is not valid"); console.error(err); } ); }, [verifyToken]); + useEffect(() => { + console.log(errorMessages); + }, [errorMessages]); + const handleSubmit = async (event: FormEvent): Promise => { event.preventDefault(); login(username, password).then( @@ -33,36 +35,54 @@ export default function Login() { await router.push("/welcome"); }, (err) => { - console.error(err); + let errorRes = err.response; + if (errorRes.status === 400) { + if (errorRes.data.non_field_errors) { + setErrorMessages(errorRes.data.non_field_errors); + } + } else { + console.error(err); + } } ); }; return ( <> - +
- My App Logo + My App Logo
- + Login.
+
+
    + { + errorMessages.map((err, i) => ( +
  • {err}
  • + )) + } +
+
+
) => setUsername(e.target.value) } @@ -75,10 +95,15 @@ export default function Login() { type="password" className={`form-control form-control-lg ${styles.input}`} value={password} - onChange={(e: React.ChangeEvent) => - setPassword(e.target.value) - } + onChange={(e: React.ChangeEvent) => { + setPassword(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Wachtwoord is verplicht."); + }} required + placeholder="Wachtwoord123" />
diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 90836337..e59be53b 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -13,6 +13,7 @@ export default function Signup() { const [email, setEmail] = useState(""); const [password1, setPassword1] = useState(""); const [password2, setPassword2] = useState(""); + const [errorMessages, setErrorMessages] = useState([]); const handleSubmit = async (event: FormEvent) => { event.preventDefault(); @@ -24,7 +25,29 @@ export default function Signup() { } }, (err) => { - console.error(err); + let errorRes = err.response; + if (errorRes.status === 400) { + let errors = []; + if (errorRes.data.firstname) { + errors.push(errorRes.data.firstname); + } + if (errorRes.data.lastname) { + errors.push(errorRes.data.lastname); + } + if (errorRes.data.email) { + errors.push(errorRes.data.email); + } + if (errorRes.data.password1) { + errors.push(errorRes.data.password1); + } + if (errorRes.data.password2) { + errors.push(errorRes.data.password2); + } + console.error(errorRes); + setErrorMessages(errors); + } else { + console.error(err); + } } ); }; @@ -48,15 +71,29 @@ export default function Signup() { Sign up.
+
+
    + { + errorMessages.map((err, i) => ( +
  • {err}
  • + )) + } +
+
+
) => - setFirstname(e.target.value) - } + onChange={(e: React.ChangeEvent) => { + setFirstname(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Voornaam is verplicht."); + }} required />
@@ -67,9 +104,13 @@ export default function Signup() { type="text" className={`form-control form-control-lg ${styles.input}`} value={lastname} - onChange={(e: React.ChangeEvent) => - setLastname(e.target.value) - } + onChange={(e: React.ChangeEvent) => { + setLastname(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Achternaam is verplicht."); + }} required />
@@ -80,10 +121,11 @@ export default function Signup() { type="email" className={`form-control form-control-lg ${styles.input}`} value={email} - onChange={(e: React.ChangeEvent) => - setEmail(e.target.value) - } + onChange={(e: React.ChangeEvent) => { + setEmail(e.target.value); + }} required + placeholder="name@example.com" />
@@ -93,10 +135,15 @@ export default function Signup() { type="password" className={`form-control form-control-lg ${styles.input}`} value={password1} - onChange={(e: React.ChangeEvent) => - setPassword1(e.target.value) - } + onChange={(e: React.ChangeEvent) => { + setPassword1(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Wachtwoord is verplicht."); + }} required + placeholder="Wachtwoord123" />
@@ -106,10 +153,24 @@ export default function Signup() { type="password" className={`form-control form-control-lg ${styles.input}`} value={password2} - onChange={(e: React.ChangeEvent) => - setPassword2(e.target.value) - } + onInput={(e: React.ChangeEvent) => { + e.target.setCustomValidity(""); + setPassword2(e.target.value); + if (password1 !== e.target.value) { + e.target.setCustomValidity("Wachtwoorden zijn niet gelijk."); + } else { + e.target.setCustomValidity(""); + } + }} + onInvalid={(e: React.ChangeEvent) => { + if (password1 !== e.target.value) { + e.target.setCustomValidity("Wachtwoorden zijn niet gelijk."); + } else { + e.target.setCustomValidity(""); + } + }} required + placeholder="Wachtwoord123" />
From db64d2a747b77ae297b3ac21bd0541f78670e0d4 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 11:25:03 +0000 Subject: [PATCH 0232/1000] Auto formatted code --- frontend/pages/login.tsx | 24 ++++++++++++------------ frontend/pages/signup.tsx | 18 +++++++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index fba33802..dd180699 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -2,15 +2,15 @@ import BaseHeader from "@/components/header/BaseHeader"; import styles from "styles/Login.module.css"; import Image from "next/image"; import filler_image from "../public/filler_image.png"; -import {login, verifyToken} from "@/lib/login"; -import React, {FormEvent, useEffect, useState} from "react"; -import {useRouter} from "next/router"; +import { login, verifyToken } from "@/lib/login"; +import React, { FormEvent, useEffect, useState } from "react"; +import { useRouter } from "next/router"; export default function Login() { const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - const [errorMessages, setErrorMessages] = useState( []); + const [errorMessages, setErrorMessages] = useState([]); // try and log in to the application using existing refresh token useEffect(() => { @@ -49,30 +49,30 @@ export default function Login() { return ( <> - +
- My App Logo + My App Logo
- + Login.
    - { - errorMessages.map((err, i) => ( -
  • {err}
  • - )) - } + {errorMessages.map((err, i) => ( +
  • + {err} +
  • + ))}
diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index e59be53b..26cf59fb 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -73,11 +73,11 @@ export default function Signup() {
    - { - errorMessages.map((err, i) => ( -
  • {err}
  • - )) - } + {errorMessages.map((err, i) => ( +
  • + {err} +
  • + ))}
@@ -157,14 +157,18 @@ export default function Signup() { e.target.setCustomValidity(""); setPassword2(e.target.value); if (password1 !== e.target.value) { - e.target.setCustomValidity("Wachtwoorden zijn niet gelijk."); + e.target.setCustomValidity( + "Wachtwoorden zijn niet gelijk." + ); } else { e.target.setCustomValidity(""); } }} onInvalid={(e: React.ChangeEvent) => { if (password1 !== e.target.value) { - e.target.setCustomValidity("Wachtwoorden zijn niet gelijk."); + e.target.setCustomValidity( + "Wachtwoorden zijn niet gelijk." + ); } else { e.target.setCustomValidity(""); } From fc10fa00763d38cfae00b4a405c798b81235b688 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Fri, 24 Mar 2023 14:39:24 +0100 Subject: [PATCH 0233/1000] #57 updated the docs to accommodate for the changes in this issue --- backend/picture_building/views.py | 55 ++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index a0ea0faf..f474fbf2 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -11,6 +11,8 @@ DESCRIPTION = 'Optionally, you can filter by date, by using the keys "from" and/or "to". When filtering, "from" and "to" are included in the result. The keys must be in format "%Y-%m-%d %H:%M:%S" or "%Y-%m-%d".' +TYPES_DESCRIPTION = "The possible types are: AA, BI, VE and OP. These stand for aankomst, binnen, vertrek and opmerkingen respectively." + TRANSLATE = {"building": "building_id"} @@ -46,7 +48,8 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] serializer_class = PictureBuildingSerializer - @extend_schema(responses={201: PictureBuildingSerializer, 400: None}) + @extend_schema(responses={201: PictureBuildingSerializer, 400: None}, + description="Create a new PictureBuilding." + TYPES_DESCRIPTION) def post(self, request): """ Create a new PictureBuilding @@ -66,7 +69,8 @@ class PictureBuildingIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding] serializer_class = PictureBuildingSerializer - @extend_schema(responses={200: PictureBuildingSerializer, 400: None}) + @extend_schema(responses={200: PictureBuildingSerializer, 400: None}, + description="Get PictureBuilding with given id." + TYPES_DESCRIPTION) def get(self, request, picture_building_id): """ Get PictureBuilding with given id @@ -82,7 +86,8 @@ def get(self, request, picture_building_id): serializer = PictureBuildingSerializer(picture_building_instance) return get_success(serializer) - @extend_schema(responses={200: PictureBuildingSerializer, 400: None}) + @extend_schema(responses={200: PictureBuildingSerializer, 400: None}, + description="Edit info about PictureBuilding with given id." + TYPES_DESCRIPTION) def patch(self, request, picture_building_id): """ Edit info about PictureBuilding with given id @@ -124,7 +129,7 @@ class PicturesOfBuildingView(APIView): serializer_class = PictureBuildingSerializer - @extend_schema(description=DESCRIPTION, responses={200: PictureBuildingSerializer, 400: None}) + @extend_schema(description=DESCRIPTION + TYPES_DESCRIPTION, responses={200: PictureBuildingSerializer, 400: None}) def get(self, request, building_id): """ Get all pictures of a building with given id @@ -152,7 +157,47 @@ class AllPictureBuildingsView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = PictureBuildingSerializer - @extend_schema(description=DESCRIPTION) + openapi_schema = { + 'operationId': 'picture_building_all_retrieve', + 'description': DESCRIPTION + TYPES_DESCRIPTION, + 'tags': ['picture-building'], + 'requestBody': { + 'content': { + 'application/json': { + 'schema': { + 'type': 'object', + 'properties': { + 'from': { + 'type': 'string', + 'format': 'date-time' + }, + 'to': { + 'type': 'string', + 'format': 'date-time' + } + }, + 'required': ['from', 'to'] + } + } + } + }, + 'security': [ + {'jwtHeaderAuth': []}, + {'jwtCookieAuth': []} + ], + 'responses': { + '200': { + 'content': { + 'application/json': { + 'schema': {'$ref': '#/components/schemas/PictureBuilding'} + } + }, + 'description': '' + } + } + } + + @extend_schema(operation=openapi_schema) def get(self, request): """ Get all pictureBuilding From d4f29937e8c42ce9c780b4807fdd4a5b6255bdf9 Mon Sep 17 00:00:00 2001 From: GashinRS Date: Fri, 24 Mar 2023 13:40:18 +0000 Subject: [PATCH 0234/1000] Auto formatted code --- backend/picture_building/views.py | 71 ++++++++++++++----------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index f474fbf2..dff3bce6 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -11,7 +11,9 @@ DESCRIPTION = 'Optionally, you can filter by date, by using the keys "from" and/or "to". When filtering, "from" and "to" are included in the result. The keys must be in format "%Y-%m-%d %H:%M:%S" or "%Y-%m-%d".' -TYPES_DESCRIPTION = "The possible types are: AA, BI, VE and OP. These stand for aankomst, binnen, vertrek and opmerkingen respectively." +TYPES_DESCRIPTION = ( + "The possible types are: AA, BI, VE and OP. These stand for aankomst, binnen, vertrek and opmerkingen respectively." +) TRANSLATE = {"building": "building_id"} @@ -48,8 +50,10 @@ class Default(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent] serializer_class = PictureBuildingSerializer - @extend_schema(responses={201: PictureBuildingSerializer, 400: None}, - description="Create a new PictureBuilding." + TYPES_DESCRIPTION) + @extend_schema( + responses={201: PictureBuildingSerializer, 400: None}, + description="Create a new PictureBuilding." + TYPES_DESCRIPTION, + ) def post(self, request): """ Create a new PictureBuilding @@ -69,8 +73,10 @@ class PictureBuildingIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding] serializer_class = PictureBuildingSerializer - @extend_schema(responses={200: PictureBuildingSerializer, 400: None}, - description="Get PictureBuilding with given id." + TYPES_DESCRIPTION) + @extend_schema( + responses={200: PictureBuildingSerializer, 400: None}, + description="Get PictureBuilding with given id." + TYPES_DESCRIPTION, + ) def get(self, request, picture_building_id): """ Get PictureBuilding with given id @@ -86,8 +92,10 @@ def get(self, request, picture_building_id): serializer = PictureBuildingSerializer(picture_building_instance) return get_success(serializer) - @extend_schema(responses={200: PictureBuildingSerializer, 400: None}, - description="Edit info about PictureBuilding with given id." + TYPES_DESCRIPTION) + @extend_schema( + responses={200: PictureBuildingSerializer, 400: None}, + description="Edit info about PictureBuilding with given id." + TYPES_DESCRIPTION, + ) def patch(self, request, picture_building_id): """ Edit info about PictureBuilding with given id @@ -158,43 +166,30 @@ class AllPictureBuildingsView(APIView): serializer_class = PictureBuildingSerializer openapi_schema = { - 'operationId': 'picture_building_all_retrieve', - 'description': DESCRIPTION + TYPES_DESCRIPTION, - 'tags': ['picture-building'], - 'requestBody': { - 'content': { - 'application/json': { - 'schema': { - 'type': 'object', - 'properties': { - 'from': { - 'type': 'string', - 'format': 'date-time' - }, - 'to': { - 'type': 'string', - 'format': 'date-time' - } + "operationId": "picture_building_all_retrieve", + "description": DESCRIPTION + TYPES_DESCRIPTION, + "tags": ["picture-building"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "from": {"type": "string", "format": "date-time"}, + "to": {"type": "string", "format": "date-time"}, }, - 'required': ['from', 'to'] + "required": ["from", "to"], } } } }, - 'security': [ - {'jwtHeaderAuth': []}, - {'jwtCookieAuth': []} - ], - 'responses': { - '200': { - 'content': { - 'application/json': { - 'schema': {'$ref': '#/components/schemas/PictureBuilding'} - } - }, - 'description': '' + "security": [{"jwtHeaderAuth": []}, {"jwtCookieAuth": []}], + "responses": { + "200": { + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PictureBuilding"}}}, + "description": "", } - } + }, } @extend_schema(operation=openapi_schema) From 1d3a6da0eeacc3b55a47c039d8792cd5e8826b25 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 15:23:46 +0100 Subject: [PATCH 0235/1000] More consistent error handling in models (fixes #111) --- backend/base/models.py | 91 +++++++++++++-------------- backend/util/request_response_util.py | 6 +- 2 files changed, 43 insertions(+), 54 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index 014f8565..8f3db3bd 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import date, datetime from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import PermissionsMixin @@ -13,13 +13,7 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2**31 - 1 - - -def _check_for_present_keys(instance, keys_iterable): - for key in keys_iterable: - if not vars(instance)[key]: - raise ValidationError(f"Tried to access {key}, but it was not found in object") +MAX_INT = 2 ** 31 - 1 class Region(models.Model): @@ -101,7 +95,6 @@ class Lobby(models.Model): role = models.ForeignKey(Role, on_delete=models.SET_NULL, null=True) def clean(self): - _check_for_present_keys(self, {"email"}) if User.objects.filter(email=self.email): raise ValidationError("Email already exists in database (the email is already linked to a user)") @@ -126,15 +119,15 @@ class Building(models.Model): def clean(self): super().clean() - # If this is not checked, `self.syndic` will cause an internal server error 500 - _check_for_present_keys(self, {"syndic_id"}) - if self.house_number == 0: raise ValidationError("The house number of the building must be positive and not zero.") - user = self.syndic - if user.role.name.lower() != "syndic": - raise ValidationError('Only a user with role "syndic" can own a building.') + # With this if, a building is not required to have a syndic. If a syndic should be required, blank has to be False + # If this if is removed, an internal server error will be thrown since youll try to access a non existing attribute of type 'NoneType' + if self.syndic: + user = self.syndic + if user.role.name.lower() != "syndic": + raise ValidationError('Only a user with role "syndic" can own a building.') # If a public_id exists, it should be unique if self.public_id: @@ -211,7 +204,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -224,8 +217,6 @@ class Tour(models.Model): def clean(self): super().clean() - _check_for_present_keys(self, {"name", "region_id"}) - if not self.modified_at: self.modified_at = str(date.today()) @@ -244,9 +235,9 @@ class Meta: class BuildingOnTour(models.Model): - tour = models.ForeignKey(Tour, on_delete=models.CASCADE) - building = models.ForeignKey(Building, on_delete=models.CASCADE) - index = models.PositiveIntegerField() + tour = models.ForeignKey(Tour, on_delete=models.CASCADE, blank=False, null=False) + building = models.ForeignKey(Building, on_delete=models.CASCADE, blank=False, null=False) + index = models.PositiveIntegerField(blank=False, null=False) """ The region of a tour and of a building needs to be the same. @@ -255,18 +246,19 @@ class BuildingOnTour(models.Model): def clean(self): super().clean() - _check_for_present_keys(self, {"tour_id", "building_id", "index"}) - - tour_region = self.tour.region - building_region = self.building.region - if tour_region != building_region: - raise ValidationError( - f"The regions for tour ({tour_region}) en building ({building_region}) are different." - ) + # Check for existence of all the fields we use below + # If the if statement fail, django will handle the errors correctly in a consistent way + if self.tour_id and self.building_id and self.index: + tour_region = self.tour.region + building_region = self.building.region + if tour_region != building_region: + raise ValidationError( + f"The regions for tour ({tour_region}) en building ({building_region}) are different." + ) - nr_of_buildings = BuildingOnTour.objects.filter(tour=self.tour).count() - if self.index > nr_of_buildings: - raise ValidationError(f"The maximum allowed index for this building is {nr_of_buildings}") + nr_of_buildings = BuildingOnTour.objects.filter(tour=self.tour_id).count() + if self.index > nr_of_buildings: + raise ValidationError(f"The maximum allowed index for this building is {nr_of_buildings}") def __str__(self): return f"{self.building} on tour {self.tour}, index: {self.index}" @@ -300,15 +292,16 @@ class StudentAtBuildingOnTour(models.Model): def clean(self): super().clean() - _check_for_present_keys(self, {"student_id", "building_on_tour_id", "date"}) - user = self.student - if user.role.name.lower() == "syndic": - raise ValidationError("A syndic can't do tours") - building_on_tour_region = self.building_on_tour.tour.region - if not self.student.region.all().filter(region=building_on_tour_region).exists(): - raise ValidationError( - f"Student ({user.email}) doesn't do tours in this region ({building_on_tour_region})." - ) + + if self.student_id and self.building_on_tour_id: + user = self.student + if user.role.name.lower() == "syndic": + raise ValidationError("A syndic can't do tours") + building_on_tour_region = self.building_on_tour.tour.region + if not self.student.region.all().filter(region=building_on_tour_region).exists(): + raise ValidationError( + f"Student ({user.email}) doesn't do tours in this region ({building_on_tour_region})." + ) class Meta: constraints = [ @@ -327,9 +320,9 @@ def __str__(self): class PictureBuilding(models.Model): building = models.ForeignKey(Building, on_delete=models.CASCADE) - picture = models.ImageField(upload_to="building_pictures/", blank=True, null=True) + picture = models.ImageField(upload_to="building_pictures/") description = models.TextField(blank=True, null=True) - timestamp = models.DateTimeField() + timestamp = models.DateTimeField(blank=True) AANKOMST = "AA" BINNEN = "BI" @@ -347,7 +340,8 @@ class PictureBuilding(models.Model): def clean(self): super().clean() - _check_for_present_keys(self, {"building_id", "picture", "description", "timestamp"}) + if not self.timestamp: + self.timestamp = datetime.now() class Meta: constraints = [ @@ -368,14 +362,13 @@ def __str__(self): class Manual(models.Model): building = models.ForeignKey(Building, on_delete=models.CASCADE) version_number = models.PositiveIntegerField(default=0) - file = models.FileField(upload_to="building_manuals/", blank=True, null=True) + file = models.FileField(upload_to="building_manuals/") def __str__(self): return f"Manual: {str(self.file).split('/')[-1]} (version {self.version_number}) for {self.building}" def clean(self): super().clean() - _check_for_present_keys(self, {"building_id", "file"}) # If no version number is given, the new version number should be the highest + 1 # If only version numbers 1, 2 and 3 are in the database, a version number of e.g. 3000 is not permitted @@ -385,9 +378,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index b2d62822..78129d27 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -54,11 +54,7 @@ def try_full_clean_and_save(model_instance, rm=False): model_instance.save() except ValidationError as e: error_message = e.message_dict - except AttributeError as e: - # If body is empty, an attribute error is thrown in the clean function - # if there is not checked whether the fields in self are intialized - error_message = str(e) + ". This error could be thrown after you passed an empty body with e.g. a POST request." - except (IntegrityError, ObjectDoesNotExist, ValueError) as e: + except Exception as e: error_message = str(e) finally: if rm: From 97d5ae0bf7d5ec822423f5109b0b01eec11c31f9 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Fri, 24 Mar 2023 14:26:44 +0000 Subject: [PATCH 0236/1000] Auto formatted code --- backend/base/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index 8f3db3bd..19b65132 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -13,7 +13,7 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2 ** 31 - 1 +MAX_INT = 2**31 - 1 class Region(models.Model): @@ -204,7 +204,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -378,9 +378,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 From bb12346d748f513bdefbf1672366c2d92dadeb58 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 15:38:31 +0100 Subject: [PATCH 0237/1000] GET request to API root returns user id in response body (fixes #116) --- backend/config/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/config/views.py b/backend/config/views.py index 3772ad31..775149d7 100644 --- a/backend/config/views.py +++ b/backend/config/views.py @@ -11,10 +11,13 @@ class RootDefault(APIView): @extend_schema( responses={200: None, 400: None, 403: None, 401: None}, - description='If you are logged in, you should see "Hello from the DrTrottoir API!".', + description='If you are logged in, you should see "Hello from the DrTrottoir API!". You should also be able to see your unique user id.', ) def get(self, request): return Response( - {"message", "Hello from the DrTrottoir API!"}, + { + "res": "Hello from the DrTrottoir API!", + "id": request.user.id + }, status=status.HTTP_200_OK, ) From 550b7a2b7e88709b3e419fb2dcd078ef4de43c99 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Fri, 24 Mar 2023 14:39:33 +0000 Subject: [PATCH 0238/1000] Auto formatted code --- backend/config/views.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/config/views.py b/backend/config/views.py index 775149d7..c7c0d1cd 100644 --- a/backend/config/views.py +++ b/backend/config/views.py @@ -15,9 +15,6 @@ class RootDefault(APIView): ) def get(self, request): return Response( - { - "res": "Hello from the DrTrottoir API!", - "id": request.user.id - }, + {"res": "Hello from the DrTrottoir API!", "id": request.user.id}, status=status.HTTP_200_OK, ) From 76dea9326cf0a41fc327105f6fc7ef56c45f07ae Mon Sep 17 00:00:00 2001 From: sevrijss Date: Fri, 24 Mar 2023 16:11:43 +0100 Subject: [PATCH 0239/1000] fix admin route django api --- backend/config/wsgi.py | 20 ++++++++++++++++++-- nginx/nginx.conf | 2 ++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/config/wsgi.py b/backend/config/wsgi.py index 1ee1797e..75f2f061 100644 --- a/backend/config/wsgi.py +++ b/backend/config/wsgi.py @@ -11,6 +11,22 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings") -application = get_wsgi_application() +_application = get_wsgi_application() + + +def application(environ, start_response): + # http://flask.pocoo.org/snippets/35/ + script_name = environ.get('HTTP_X_SCRIPT_NAME', '') + if script_name: + environ['SCRIPT_NAME'] = script_name + path_info = environ['PATH_INFO'] + if path_info.startswith(script_name): + environ['PATH_INFO'] = path_info[len(script_name):] + + scheme = environ.get('HTTP_X_SCHEME', '') + if scheme: + environ['wsgi.url_scheme'] = scheme + + return _application(environ, start_response) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 2ebd9b04..0d9cb134 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -30,6 +30,8 @@ http { # for development purposes location ~ ^/api/(?.*)$ { proxy_pass http://docker-backend/$rest; + proxy_set_header X-Script-Name /api; + proxy_cookie_path / /api; } location / { proxy_pass http://docker-frontend; From 43bc8db99204f29b573db9492b23b1e95ba0ac07 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Fri, 24 Mar 2023 15:13:35 +0000 Subject: [PATCH 0240/1000] Auto formatted code --- backend/config/wsgi.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/config/wsgi.py b/backend/config/wsgi.py index 75f2f061..18a88dc6 100644 --- a/backend/config/wsgi.py +++ b/backend/config/wsgi.py @@ -18,15 +18,15 @@ def application(environ, start_response): # http://flask.pocoo.org/snippets/35/ - script_name = environ.get('HTTP_X_SCRIPT_NAME', '') + script_name = environ.get("HTTP_X_SCRIPT_NAME", "") if script_name: - environ['SCRIPT_NAME'] = script_name - path_info = environ['PATH_INFO'] + environ["SCRIPT_NAME"] = script_name + path_info = environ["PATH_INFO"] if path_info.startswith(script_name): - environ['PATH_INFO'] = path_info[len(script_name):] + environ["PATH_INFO"] = path_info[len(script_name) :] - scheme = environ.get('HTTP_X_SCHEME', '') + scheme = environ.get("HTTP_X_SCHEME", "") if scheme: - environ['wsgi.url_scheme'] = scheme + environ["wsgi.url_scheme"] = scheme return _application(environ, start_response) From ee297d6bc6d8cd88d58982b676f726d2fd36ad04 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 16:13:57 +0100 Subject: [PATCH 0241/1000] Apply workaround for GET request body documentation to PicturesOfBuildingView as well (#57) --- backend/picture_building/views.py | 78 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index dff3bce6..5a20f867 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -1,20 +1,59 @@ +from datetime import datetime + from django.db.models import QuerySet +from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from base.permissions import IsAdmin, IsSuperStudent, IsStudent, ReadOnlyOwnerOfBuilding from base.models import PictureBuilding, Building +from base.permissions import IsAdmin, IsSuperStudent, IsStudent, ReadOnlyOwnerOfBuilding from base.serializers import PictureBuildingSerializer from util.request_response_util import * -from drf_spectacular.utils import extend_schema -from datetime import datetime DESCRIPTION = 'Optionally, you can filter by date, by using the keys "from" and/or "to". When filtering, "from" and "to" are included in the result. The keys must be in format "%Y-%m-%d %H:%M:%S" or "%Y-%m-%d".' TYPES_DESCRIPTION = ( - "The possible types are: AA, BI, VE and OP. These stand for aankomst, binnen, vertrek and opmerkingen respectively." + 'The possible types are: "AA", "BI", "VE" and "OP". These stand for aankomst, binnen, vertrek and opmerkingen respectively.' ) + +# Swagger cannot generate a request body for GET. This is a workaround. +def _get_open_api_schema(include_400=False): + res_200 = { + "200": { + "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PictureBuilding"}}}, + "description": "" + } + } + + res_400 = { + "200": res_200["200"], + "400": {"description": "No response body"} + } + + return { + "operationId": "picture_building_all_retrieve", + "description": DESCRIPTION + TYPES_DESCRIPTION, + "tags": ["picture-building"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "from": {"type": "string", "format": "date-time"}, + "to": {"type": "string", "format": "date-time"}, + }, + "required": ["from", "to"], + } + } + } + }, + "security": [{"jwtHeaderAuth": []}, {"jwtCookieAuth": []}], + "responses": res_400 if include_400 else res_200 + } + + TRANSLATE = {"building": "building_id"} @@ -137,7 +176,7 @@ class PicturesOfBuildingView(APIView): serializer_class = PictureBuildingSerializer - @extend_schema(description=DESCRIPTION + TYPES_DESCRIPTION, responses={200: PictureBuildingSerializer, 400: None}) + @extend_schema(operation=_get_open_api_schema(True)) def get(self, request, building_id): """ Get all pictures of a building with given id @@ -165,34 +204,7 @@ class AllPictureBuildingsView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = PictureBuildingSerializer - openapi_schema = { - "operationId": "picture_building_all_retrieve", - "description": DESCRIPTION + TYPES_DESCRIPTION, - "tags": ["picture-building"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "from": {"type": "string", "format": "date-time"}, - "to": {"type": "string", "format": "date-time"}, - }, - "required": ["from", "to"], - } - } - } - }, - "security": [{"jwtHeaderAuth": []}, {"jwtCookieAuth": []}], - "responses": { - "200": { - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PictureBuilding"}}}, - "description": "", - } - }, - } - - @extend_schema(operation=openapi_schema) + @extend_schema(operation=_get_open_api_schema()) def get(self, request): """ Get all pictureBuilding From 3aaea5c408331a677658e86ad0620bbffb25006f Mon Sep 17 00:00:00 2001 From: TiboStr Date: Fri, 24 Mar 2023 15:14:54 +0000 Subject: [PATCH 0242/1000] Auto formatted code --- backend/picture_building/views.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 5a20f867..26d614ef 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -12,9 +12,7 @@ DESCRIPTION = 'Optionally, you can filter by date, by using the keys "from" and/or "to". When filtering, "from" and "to" are included in the result. The keys must be in format "%Y-%m-%d %H:%M:%S" or "%Y-%m-%d".' -TYPES_DESCRIPTION = ( - 'The possible types are: "AA", "BI", "VE" and "OP". These stand for aankomst, binnen, vertrek and opmerkingen respectively.' -) +TYPES_DESCRIPTION = 'The possible types are: "AA", "BI", "VE" and "OP". These stand for aankomst, binnen, vertrek and opmerkingen respectively.' # Swagger cannot generate a request body for GET. This is a workaround. @@ -22,14 +20,11 @@ def _get_open_api_schema(include_400=False): res_200 = { "200": { "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PictureBuilding"}}}, - "description": "" + "description": "", } } - res_400 = { - "200": res_200["200"], - "400": {"description": "No response body"} - } + res_400 = {"200": res_200["200"], "400": {"description": "No response body"}} return { "operationId": "picture_building_all_retrieve", @@ -50,7 +45,7 @@ def _get_open_api_schema(include_400=False): } }, "security": [{"jwtHeaderAuth": []}, {"jwtCookieAuth": []}], - "responses": res_400 if include_400 else res_200 + "responses": res_400 if include_400 else res_200, } From 73003069b1bf541b53e7e0180d3b0f3977807cef Mon Sep 17 00:00:00 2001 From: sevrijss Date: Fri, 24 Mar 2023 16:16:39 +0100 Subject: [PATCH 0243/1000] apply to port 443 as well --- nginx/nginx.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 0d9cb134..0cdc3502 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -46,7 +46,8 @@ http { location ~ ^/api/(?.*)$ { proxy_pass http://docker-backend/$rest; -# proxy_redirect http:// https://; + proxy_set_header X-Script-Name /api; + proxy_cookie_path / /api; } location / { From 322090dc107a98c1b0701ab66e2441e9129bcec2 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Fri, 24 Mar 2023 16:27:19 +0100 Subject: [PATCH 0244/1000] removed unnecessary gitignore line --- frontend/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index 8a1beb08..c87c9b39 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -34,4 +34,3 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -/env.local From cf7f337972fb6c1a8600fefc8e746f84b7a0d32f Mon Sep 17 00:00:00 2001 From: sevrijss Date: Fri, 24 Mar 2023 16:28:31 +0100 Subject: [PATCH 0245/1000] open port 2002 to backend for a nicer ui --- nginx/nginx.conf | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 2ebd9b04..45801a17 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -57,4 +57,13 @@ http { # ssl_certificate_key '/etc/letsencrypt/live/sel2-4.ugent.be/privkey.pem'; # include /etc/letsencrypt/options-ssl-nginx.conf; } + + server { + listen 2002; + server_name localhost; + location / { + proxy_pass http://docker-backend; + proxy_redirect off; + } + } } \ No newline at end of file From e8b5aa71f8244e39a7604fe3ec71406601d9ec51 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Fri, 24 Mar 2023 16:34:49 +0100 Subject: [PATCH 0246/1000] #88 split get_patch_docs() into get_docs() and patch_docs() --- backend/building/views.py | 8 ++++---- backend/building_comment/views.py | 6 +++--- backend/building_on_tour/views.py | 4 ++-- backend/email_template/views.py | 4 ++-- backend/garbage_collection/views.py | 4 ++-- backend/lobby/views.py | 2 +- backend/manual/views.py | 6 +++--- backend/picture_building/views.py | 4 ++-- backend/region/views.py | 4 ++-- backend/role/views.py | 4 ++-- backend/student_at_building_on_tour/views.py | 4 ++-- backend/tour/views.py | 4 ++-- backend/users/views.py | 4 ++-- backend/util/request_response_util.py | 6 +++++- 14 files changed, 34 insertions(+), 30 deletions(-) diff --git a/backend/building/views.py b/backend/building/views.py index f75e3805..9c1a4eff 100644 --- a/backend/building/views.py +++ b/backend/building/views.py @@ -38,7 +38,7 @@ class BuildingIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyOwnerOfBuilding] serializer_class = BuildingSerializer - @extend_schema(responses=get_patch_docs(BuildingSerializer)) + @extend_schema(responses=get_docs(BuildingSerializer)) def get(self, request, building_id): """ Get info about building with given id @@ -68,7 +68,7 @@ def delete(self, request, building_id): building_instance.delete() return delete_success() - @extend_schema(responses=get_patch_docs(BuildingSerializer)) + @extend_schema(responses=patch_docs(BuildingSerializer)) def patch(self, request, building_id): """ Edit building with given ID @@ -93,7 +93,7 @@ def patch(self, request, building_id): class BuildingPublicView(APIView): serializer_class = BuildingSerializer - @extend_schema(responses=get_patch_docs(BuildingSerializer)) + @extend_schema(responses=get_docs(BuildingSerializer)) def get(self, request, building_public_id): """ Get building with the public id @@ -154,7 +154,7 @@ class BuildingOwnerView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerOfBuilding] serializer_class = BuildingSerializer - @extend_schema(responses=get_patch_docs(BuildingSerializer)) + @extend_schema(responses=get_docs(BuildingSerializer)) def get(self, request, owner_id): """ Get all buildings owned by syndic with given id diff --git a/backend/building_comment/views.py b/backend/building_comment/views.py index b35ff5ba..ac6228c5 100644 --- a/backend/building_comment/views.py +++ b/backend/building_comment/views.py @@ -37,7 +37,7 @@ class BuildingCommentIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent] serializer_class = BuildingCommentSerializer - @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) + @extend_schema(responses=get_docs(BuildingCommentSerializer)) def get(self, request, building_comment_id): """ Get an invividual BuildingComment with given id @@ -66,7 +66,7 @@ def delete(self, request, building_comment_id): building_comment_instance[0].delete() return delete_success() - @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) + @extend_schema(responses=patch_docs(BuildingCommentSerializer)) def patch(self, request, building_comment_id): """ Edit BuildingComment with given id @@ -93,7 +93,7 @@ class BuildingCommentBuildingView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | OwnerOfBuilding | ReadOnlyStudent] serializer_class = BuildingCommentSerializer - @extend_schema(responses=get_patch_docs(BuildingCommentSerializer)) + @extend_schema(responses=get_docs(BuildingCommentSerializer)) def get(self, request, building_id): """ Get all BuildingComments of building with given building id diff --git a/backend/building_on_tour/views.py b/backend/building_on_tour/views.py index d83aa7c1..3d51050d 100644 --- a/backend/building_on_tour/views.py +++ b/backend/building_on_tour/views.py @@ -35,7 +35,7 @@ class BuildingTourIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = BuildingTourSerializer - @extend_schema(responses=get_patch_docs(BuildingTourSerializer)) + @extend_schema(responses=get_docs(BuildingTourSerializer)) def get(self, request, building_tour_id): """ Get info about a BuildingOnTour with given id @@ -48,7 +48,7 @@ def get(self, request, building_tour_id): serializer = BuildingTourSerializer(building_on_tour_instance[0]) return get_success(serializer) - @extend_schema(responses=get_patch_docs(BuildingTourSerializer)) + @extend_schema(responses=patch_docs(BuildingTourSerializer)) def patch(self, request, building_tour_id): """ edit info about a BuildingOnTour with given id diff --git a/backend/email_template/views.py b/backend/email_template/views.py index d90e1b31..043e46ec 100644 --- a/backend/email_template/views.py +++ b/backend/email_template/views.py @@ -33,7 +33,7 @@ class EmailTemplateIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = EmailTemplateSerializer - @extend_schema(responses=get_patch_docs(EmailTemplateSerializer)) + @extend_schema(responses=get_docs(EmailTemplateSerializer)) def get(self, request, email_template_id): """ Get info about an EmailTemplate with given id @@ -58,7 +58,7 @@ def delete(self, request, email_template_id): email_template_instance[0].delete() return delete_success() - @extend_schema(responses=get_patch_docs(EmailTemplateSerializer)) + @extend_schema(responses=patch_docs(EmailTemplateSerializer)) def patch(self, request, email_template_id): """ Edit EmailTemplate with given id diff --git a/backend/garbage_collection/views.py b/backend/garbage_collection/views.py index e2304ba7..9ebfb9d3 100644 --- a/backend/garbage_collection/views.py +++ b/backend/garbage_collection/views.py @@ -36,7 +36,7 @@ class GarbageCollectionIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = GarbageCollectionSerializer - @extend_schema(responses=get_patch_docs(GarbageCollectionSerializer)) + @extend_schema(responses=get_docs(GarbageCollectionSerializer)) def get(self, request, garbage_collection_id): """ Get info about a garbage collection with given id @@ -58,7 +58,7 @@ def delete(self, request, garbage_collection_id): garbage_collection_instance[0].delete() return delete_success() - @extend_schema(responses=get_patch_docs(GarbageCollectionSerializer)) + @extend_schema(responses=patch_docs(GarbageCollectionSerializer)) def patch(self, request, garbage_collection_id): """ Edit garbage collection with given id diff --git a/backend/lobby/views.py b/backend/lobby/views.py index 4116422e..be270847 100644 --- a/backend/lobby/views.py +++ b/backend/lobby/views.py @@ -47,7 +47,7 @@ class LobbyIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = LobbySerializer - @extend_schema(responses=get_patch_docs(LobbySerializer)) + @extend_schema(responses=get_docs(LobbySerializer)) def get(self, request, lobby_id): """ Get info about an EmailWhitelist with given id diff --git a/backend/manual/views.py b/backend/manual/views.py index c2dba7f8..69ddaaad 100644 --- a/backend/manual/views.py +++ b/backend/manual/views.py @@ -41,7 +41,7 @@ class ManualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | ReadOnlyManualFromSyndic] serializer_class = ManualSerializer - @extend_schema(responses=get_patch_docs(ManualSerializer)) + @extend_schema(responses=get_docs(ManualSerializer)) def get(self, request, manual_id): """ Get info about a manual with given id @@ -67,7 +67,7 @@ def delete(self, request, manual_id): manual_instances[0].delete() return delete_success() - @extend_schema(responses=get_patch_docs(ManualSerializer)) + @extend_schema(responses=patch_docs(ManualSerializer)) def patch(self, request, manual_id): """ Edit info about a manual with given id @@ -90,7 +90,7 @@ class ManualBuildingView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent | OwnerOfBuilding] serializer_class = ManualSerializer - @extend_schema(responses=get_patch_docs(ManualSerializer)) + @extend_schema(responses=get_docs(ManualSerializer)) def get(self, request, building_id): """ Get all manuals of a building with given id diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 02259846..091cd5da 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -34,7 +34,7 @@ class PictureBuildingIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | IsStudent | ReadOnlyOwnerOfBuilding] serializer_class = PictureBuildingSerializer - @extend_schema(responses=get_patch_docs(PictureBuildingSerializer)) + @extend_schema(responses=get_docs(PictureBuildingSerializer)) def get(self, request, picture_building_id): """ Get PictureBuilding with given id @@ -50,7 +50,7 @@ def get(self, request, picture_building_id): serializer = PictureBuildingSerializer(picture_building_instance) return get_success(serializer) - @extend_schema(responses=get_patch_docs(PictureBuildingSerializer)) + @extend_schema(responses=patch_docs(PictureBuildingSerializer)) def patch(self, request, picture_building_id): """ Edit info about PictureBuilding with given id diff --git a/backend/region/views.py b/backend/region/views.py index 1158aff6..3380341c 100644 --- a/backend/region/views.py +++ b/backend/region/views.py @@ -34,7 +34,7 @@ class RegionIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | ReadOnly] serializer_class = RegionSerializer - @extend_schema(responses=get_patch_docs(RegionSerializer)) + @extend_schema(responses=get_docs(RegionSerializer)) def get(self, request, region_id): """ Get info about a Region with given id @@ -47,7 +47,7 @@ def get(self, request, region_id): serializer = RegionSerializer(region_instance[0]) return get_success(serializer) - @extend_schema(responses=get_patch_docs(RegionSerializer)) + @extend_schema(responses=patch_docs(RegionSerializer)) def patch(self, request, region_id): """ Edit Region with given id diff --git a/backend/role/views.py b/backend/role/views.py index dcd786f3..459e3433 100644 --- a/backend/role/views.py +++ b/backend/role/views.py @@ -33,7 +33,7 @@ class RoleIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] serializer_class = RoleSerializer - @extend_schema(responses=get_patch_docs(RoleSerializer)) + @extend_schema(responses=get_docs(RoleSerializer)) def get(self, request, role_id): """ Get info about a Role with given id @@ -59,7 +59,7 @@ def delete(self, request, role_id): role_instance[0].delete() return delete_success() - @extend_schema(responses=get_patch_docs(RoleSerializer)) + @extend_schema(responses=patch_docs(RoleSerializer)) def patch(self, request, role_id): """ Edit info about a Role with given id diff --git a/backend/student_at_building_on_tour/views.py b/backend/student_at_building_on_tour/views.py index b3ad8423..f804ff79 100644 --- a/backend/student_at_building_on_tour/views.py +++ b/backend/student_at_building_on_tour/views.py @@ -50,7 +50,7 @@ class StudentAtBuildingOnTourIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyOwnerAccount] serializer_class = StudBuildTourSerializer - @extend_schema(responses=get_patch_docs(StudBuildTourSerializer)) + @extend_schema(responses=get_docs(StudBuildTourSerializer)) def get(self, request, student_at_building_on_tour_id): """ Get an individual StudentAtBuildingOnTour with given id @@ -66,7 +66,7 @@ def get(self, request, student_at_building_on_tour_id): serializer = StudBuildTourSerializer(stud_tour_building_instance) return get_success(serializer) - @extend_schema(responses=get_patch_docs(StudBuildTourSerializer)) + @extend_schema(responses=patch_docs(StudBuildTourSerializer)) def patch(self, request, student_at_building_on_tour_id): """ Edit info about an individual StudentAtBuildingOnTour with given id diff --git a/backend/tour/views.py b/backend/tour/views.py index 65bcf474..f650a7d8 100644 --- a/backend/tour/views.py +++ b/backend/tour/views.py @@ -34,7 +34,7 @@ class TourIndividualView(APIView): permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent | ReadOnlyStudent] serializer_class = TourSerializer - @extend_schema(responses=get_patch_docs(TourSerializer)) + @extend_schema(responses=get_docs(TourSerializer)) def get(self, request, tour_id): """ Get info about a Tour with given id @@ -48,7 +48,7 @@ def get(self, request, tour_id): serializer = TourSerializer(tour_instance) return get_success(serializer) - @extend_schema(responses=get_patch_docs(TourSerializer)) + @extend_schema(responses=patch_docs(TourSerializer)) def patch(self, request, tour_id): """ Edit a tour with given id diff --git a/backend/users/views.py b/backend/users/views.py index ac9f1cdf..dad10b95 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -84,7 +84,7 @@ class UserIndividualView(APIView): ] serializer_class = UserSerializer - @extend_schema(responses=get_patch_docs(UserSerializer)) + @extend_schema(responses=get_docs(UserSerializer)) def get(self, request, user_id): """ Get info about user with given id @@ -118,7 +118,7 @@ def delete(self, request, user_id): return delete_success() - @extend_schema(responses=get_patch_docs(UserSerializer)) + @extend_schema(responses=patch_docs(UserSerializer)) def patch(self, request, user_id): """ Edit user with given id diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index 84422baa..ffc4c288 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -101,6 +101,10 @@ def delete_docs(): return {204: None, 400: None} -def get_patch_docs(serializer): +def get_docs(serializer): return {200: serializer, 400: None} + +def patch_docs(serializer): + return get_docs(serializer) + From b135781f9f88f234918e25dc3779b56423550f86 Mon Sep 17 00:00:00 2001 From: GashinRS Date: Fri, 24 Mar 2023 15:36:21 +0000 Subject: [PATCH 0247/1000] Auto formatted code --- backend/util/request_response_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index ffc4c288..44dcaaad 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -107,4 +107,3 @@ def get_docs(serializer): def patch_docs(serializer): return get_docs(serializer) - From eb46ec65873cd021755d3bdb6810e2b7ac290de6 Mon Sep 17 00:00:00 2001 From: sevrijss Date: Fri, 24 Mar 2023 17:22:52 +0100 Subject: [PATCH 0248/1000] fix trailing / on root of api --- nginx/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 2ebd9b04..76361db7 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -28,7 +28,7 @@ http { listen 80 default_server; listen [::]:80; # for development purposes - location ~ ^/api/(?.*)$ { + location ~ ^/api(/?|/(?.*))$ { proxy_pass http://docker-backend/$rest; } location / { From c28f0307ceea1ef6a0a0058feea885acd99fb9c8 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Fri, 24 Mar 2023 18:01:29 +0100 Subject: [PATCH 0249/1000] #57 removed required field in docs --- backend/picture_building/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 26d614ef..97d3f708 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -38,8 +38,7 @@ def _get_open_api_schema(include_400=False): "properties": { "from": {"type": "string", "format": "date-time"}, "to": {"type": "string", "format": "date-time"}, - }, - "required": ["from", "to"], + } } } } From 0699266274e6107997385daf01af23f3bdb82194 Mon Sep 17 00:00:00 2001 From: GashinRS Date: Fri, 24 Mar 2023 17:02:10 +0000 Subject: [PATCH 0250/1000] Auto formatted code --- backend/picture_building/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/picture_building/views.py b/backend/picture_building/views.py index 97d3f708..8fa55353 100644 --- a/backend/picture_building/views.py +++ b/backend/picture_building/views.py @@ -38,7 +38,7 @@ def _get_open_api_schema(include_400=False): "properties": { "from": {"type": "string", "format": "date-time"}, "to": {"type": "string", "format": "date-time"}, - } + }, } } } From 2a159c8e39ab7d5ea1b57c73071645c6f461f9f4 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 19:03:08 +0100 Subject: [PATCH 0251/1000] Removed logs + some renaming --- frontend/pages/login.tsx | 8 ++------ frontend/pages/reset-password.tsx | 2 ++ frontend/pages/signup.tsx | 8 +++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index fba33802..59cbf622 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -24,10 +24,6 @@ export default function Login() { ); }, [verifyToken]); - useEffect(() => { - console.log(errorMessages); - }, [errorMessages]); - const handleSubmit = async (event: FormEvent): Promise => { event.preventDefault(); login(username, password).then( @@ -82,7 +78,7 @@ export default function Login() { type="email" className={`form-control form-control-lg ${styles.input}`} value={username} - placeholder="name@example.com" + placeholder="naam@voorbeeld.com" onChange={(e: React.ChangeEvent) => setUsername(e.target.value) } @@ -103,7 +99,7 @@ export default function Login() { e.target.setCustomValidity("Wachtwoord is verplicht."); }} required - placeholder="Wachtwoord123" + placeholder="Wachtwoord" />
diff --git a/frontend/pages/reset-password.tsx b/frontend/pages/reset-password.tsx index e63119df..0cb0b65f 100644 --- a/frontend/pages/reset-password.tsx +++ b/frontend/pages/reset-password.tsx @@ -55,6 +55,8 @@ export default function ResetPassword() { onChange={(e: React.ChangeEvent) => setEmail(e.target.value) } + required + placeholder="naam@voorbeeld.com" />
diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index e59be53b..ad711eed 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -95,6 +95,7 @@ export default function Signup() { e.target.setCustomValidity("Voornaam is verplicht."); }} required + placeholder="Voornaam" />
@@ -112,6 +113,7 @@ export default function Signup() { e.target.setCustomValidity("Achternaam is verplicht."); }} required + placeholder="Achternaam" />
@@ -125,7 +127,7 @@ export default function Signup() { setEmail(e.target.value); }} required - placeholder="name@example.com" + placeholder="naam@voorbeeld.com" />
@@ -143,7 +145,7 @@ export default function Signup() { e.target.setCustomValidity("Wachtwoord is verplicht."); }} required - placeholder="Wachtwoord123" + placeholder="Wachtwoord" />
@@ -170,7 +172,7 @@ export default function Signup() { } }} required - placeholder="Wachtwoord123" + placeholder="Wachtwoord" />
From 5bff96ec595ebaa99bb8537122e7b713a6a064b8 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 19:05:09 +0100 Subject: [PATCH 0252/1000] Removed error --- frontend/pages/signup.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index ad711eed..5e1a3373 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -43,7 +43,6 @@ export default function Signup() { if (errorRes.data.password2) { errors.push(errorRes.data.password2); } - console.error(errorRes); setErrorMessages(errors); } else { console.error(err); From bdf89562c6a45347bce35d826c30e714dda6c0e4 Mon Sep 17 00:00:00 2001 From: Sheng Tao Date: Fri, 24 Mar 2023 21:22:21 +0100 Subject: [PATCH 0253/1000] #106 changed build to rebuild in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e63fd940..2488a305 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Whenever you need to rebuild your containers, just use: docker-compose build ``` -Or if you want to build and then run at the same time, use: +Or if you want to rebuild and then run at the same time, use: ```bash docker-compose up --build -d ``` From 629279cff8948c43ce32e38fd31fca94b5e08c62 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Fri, 24 Mar 2023 22:06:58 +0100 Subject: [PATCH 0254/1000] CustomRegisterView + CustomRegisterSerializer (fixed) --- backend/authentication/serializers.py | 20 +++++++++----------- backend/authentication/views.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index a8718015..5a2ade7b 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -1,21 +1,19 @@ from dj_rest_auth.registration.serializers import RegisterSerializer -from phonenumber_field.serializerfields import PhoneNumberField -from rest_framework.serializers import CharField, IntegerField from base.models import User from util.request_response_util import request_to_dict class CustomRegisterSerializer(RegisterSerializer): - first_name = CharField(required=True) - last_name = CharField(required=True) - phone_number = PhoneNumberField(required=True) - role = IntegerField(required=True) def custom_signup(self, request, user: User): data = request_to_dict(request.data) - - user.first_name = data['first_name'] - user.last_name = data['last_name'] - user.phone_number = data['phone_number'] - user.role_id = data['role'] + user.first_name = data.get('first_name') + user.last_name = data.get('last_name') + user.phone_number = data.get('phone_number') + user.role_id = data.get('role') + region = data.get('region') + if region: + # TODO: change this to the same util function as used in user view + user.region = data.get('region') + user.save() diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 34454915..7c3a1f3a 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -35,17 +35,24 @@ def post(self, request): lobby_instances = Lobby.objects.filter(email=data.get('email')) if not lobby_instances: return Response( - {"res": f"There is no entry in the lobby for email address: {data.get('email')}"}, + { + "error": "Unauthorized signup", + "message": f"The given email address {data.get('email')} has no entry in the lobby. You must contact an admin to gain access to the platform." + }, status=status.HTTP_403_FORBIDDEN ) lobby_instance = lobby_instances[0] # check if the verification code is valid if lobby_instance.verification_code != data.get('verification_code'): return Response( - {"res": "Invalid verification code"}, + { + "error": "Unauthorized signup", + "message": "Invalid verification code" + }, status=status.HTTP_403_FORBIDDEN ) + # add the role to the request, as this was already set by an admin request.data._mutable = True request.data['role'] = lobby_instance.role_id request.data._mutable = False From e964532bbb843b06b4068e8b791f197344d676af Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 22:15:09 +0100 Subject: [PATCH 0255/1000] In response bodies: res -> message (fixes #123) --- backend/config/views.py | 2 +- backend/util/request_response_util.py | 6 +++--- readme/API-docs.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/config/views.py b/backend/config/views.py index c7c0d1cd..f7c48cb5 100644 --- a/backend/config/views.py +++ b/backend/config/views.py @@ -15,6 +15,6 @@ class RootDefault(APIView): ) def get(self, request): return Response( - {"res": "Hello from the DrTrottoir API!", "id": request.user.id}, + {"message": "Hello from the DrTrottoir API!", "id": request.user.id}, status=status.HTTP_200_OK, ) diff --git a/backend/util/request_response_util.py b/backend/util/request_response_util.py index c8cdbecc..d831fa25 100644 --- a/backend/util/request_response_util.py +++ b/backend/util/request_response_util.py @@ -31,18 +31,18 @@ def set_keys_of_instance(instance, data: dict, translation: dict = {}): def bad_request(object_name="Object"): return Response( - {"res": f"{object_name} with given ID does not exist."}, + {"message": f"{object_name} with given ID does not exist."}, status=status.HTTP_400_BAD_REQUEST, ) def not_found(object_name="Object"): - return Response({"res": f"{object_name} with given ID does not exists."}, status=status.HTTP_400_BAD_REQUEST) + return Response({"message": f"{object_name} with given ID does not exists."}, status=status.HTTP_400_BAD_REQUEST) def bad_request_relation(object1: str, object2: str): return Response( - {"res": f"There is no {object1} that is linked to {object2} with given id."}, + {"message": f"There is no {object1} that is linked to {object2} with given id."}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/readme/API-docs.md b/readme/API-docs.md index e01027fc..b01efb34 100644 --- a/readme/API-docs.md +++ b/readme/API-docs.md @@ -41,7 +41,7 @@ class AllUsersView(APIView): if not user_instances: return Response( - {"res": "No users found"}, + {"message": "No users found"}, status=status.HTTP_400_BAD_REQUEST ) From b0acbfab1ec08cb27bd516ae137c143e24a7717c Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 22:29:00 +0100 Subject: [PATCH 0256/1000] Integrated i18n in frontend + added i18n to /login --- frontend/i18n.tsx | 20 +++++++ frontend/locales/nl.json | 3 + frontend/package-lock.json | 120 +++++++++++++++++++++++++++++++++++++ frontend/package.json | 2 + frontend/pages/_app.tsx | 14 +++-- frontend/pages/login.tsx | 7 ++- 6 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 frontend/i18n.tsx create mode 100644 frontend/locales/nl.json diff --git a/frontend/i18n.tsx b/frontend/i18n.tsx new file mode 100644 index 00000000..aebfc76d --- /dev/null +++ b/frontend/i18n.tsx @@ -0,0 +1,20 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import translationNL from './locales/nl.json'; + +const resources = { + nl: { + translation: translationNL, + }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: 'nl', // set the default language here + interpolation: { + escapeValue: false, + }, +}); + +export default i18n; \ No newline at end of file diff --git a/frontend/locales/nl.json b/frontend/locales/nl.json new file mode 100644 index 00000000..7df1aa68 --- /dev/null +++ b/frontend/locales/nl.json @@ -0,0 +1,3 @@ +{ + "Unable to log in with provided credentials.": "E-mail of wachtwoord is niet juist." +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f6a1fc70..d68354cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,10 +14,12 @@ "axios": "^1.3.4", "bootstrap": "^5.2.3", "dotenv": "^16.0.3", + "i18next": "^22.4.13", "js-cookie": "^3.0.1", "next": "^13.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-i18next": "^12.2.0", "swr": "^2.0.4", "typescript": "4.9.5" }, @@ -26,6 +28,17 @@ "@types/js-cookie": "^3.0.3" } }, + "node_modules/@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@next/env": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.2.1.tgz", @@ -408,6 +421,36 @@ "node": ">= 6" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "22.4.13", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.13.tgz", + "integrity": "sha512-GX7flMHRRqQA0I1yGLmaZ4Hwt1JfLqagk8QPDPZsqekbKtXsuIngSVWM/s3SLgNkrEXjA+0sMGNuOEkkmyqmWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, "node_modules/js-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", @@ -573,6 +616,32 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", + "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -649,9 +718,25 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } } }, "dependencies": { + "@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, "@next/env": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.2.1.tgz", @@ -865,6 +950,22 @@ "mime-types": "^2.1.12" } }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, + "i18next": { + "version": "22.4.13", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.13.tgz", + "integrity": "sha512-GX7flMHRRqQA0I1yGLmaZ4Hwt1JfLqagk8QPDPZsqekbKtXsuIngSVWM/s3SLgNkrEXjA+0sMGNuOEkkmyqmWg==", + "requires": { + "@babel/runtime": "^7.20.6" + } + }, "js-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", @@ -963,6 +1064,20 @@ "scheduler": "^0.23.0" } }, + "react-i18next": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.0.tgz", + "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==", + "requires": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -1007,6 +1122,11 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "requires": {} + }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" } } } diff --git a/frontend/package.json b/frontend/package.json index dc840bd5..afba857d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,10 +15,12 @@ "axios": "^1.3.4", "bootstrap": "^5.2.3", "dotenv": "^16.0.3", + "i18next": "^22.4.13", "js-cookie": "^3.0.1", "next": "^13.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-i18next": "^12.2.0", "swr": "^2.0.4", "typescript": "4.9.5" }, diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 8dc9a776..e0725270 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,13 +1,19 @@ import "bootstrap/dist/css/bootstrap.min.css"; import "@/styles/globals.css"; -import type { AppProps } from "next/app"; -import { useEffect } from "react"; +import type {AppProps} from "next/app"; +import {useEffect} from "react"; +import {I18nextProvider} from 'react-i18next'; +import i18n from '../i18n'; -export default function App({ Component, pageProps }: AppProps) { +export default function App({Component, pageProps}: AppProps) { useEffect(() => { require("bootstrap/dist/js/bootstrap.bundle.min.js"); }, []); - return ; + return ( + + + + ); } diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 59b784a3..4b1d8306 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -3,10 +3,12 @@ import styles from "styles/Login.module.css"; import Image from "next/image"; import filler_image from "../public/filler_image.png"; import { login, verifyToken } from "@/lib/login"; -import React, { FormEvent, useEffect, useState } from "react"; +import React, {FormEvent, useEffect, useState} from "react"; import { useRouter } from "next/router"; +import { useTranslation } from 'react-i18next'; export default function Login() { + const { t } = useTranslation(); const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -66,12 +68,11 @@ export default function Login() {
    {errorMessages.map((err, i) => (
  • - {err} + {t(err)}
  • ))}
-
Date: Fri, 24 Mar 2023 22:39:02 +0100 Subject: [PATCH 0257/1000] Error handling when adding inactive email to lobby (fixes #125) --- backend/base/models.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index 014f8565..70a6f6e9 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -13,7 +13,7 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2**31 - 1 +MAX_INT = 2 ** 31 - 1 def _check_for_present_keys(instance, keys_iterable): @@ -81,7 +81,7 @@ class User(AbstractBaseUser, PermissionsMixin): region = models.ManyToManyField(Region) # This is the new role model - role = models.ForeignKey(Role, on_delete=models.SET_NULL, null=True, blank=True) + role = models.ForeignKey(Role, on_delete=models.SET_NULL, null=True) objects = UserManager() @@ -102,8 +102,14 @@ class Lobby(models.Model): def clean(self): _check_for_present_keys(self, {"email"}) - if User.objects.filter(email=self.email): - raise ValidationError("Email already exists in database (the email is already linked to a user)") + user = User.objects.filter(email=self.email) + if user: + user = user[0] + is_inactive = not user.is_active + addendum = "" + if is_inactive: + addendum = " This email belongs to an INACTIVE user. Instead of trying to register this user, you can simply reactivate the account." + raise ValidationError(f"Email already exists in database for a user (id: {user.id}).{addendum}") class Building(models.Model): @@ -211,7 +217,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -385,9 +391,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 From 353b43e871c34bb08298e0d9ec108707c6aa7173 Mon Sep 17 00:00:00 2001 From: TiboStr Date: Fri, 24 Mar 2023 21:44:19 +0000 Subject: [PATCH 0258/1000] Auto formatted code --- backend/base/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/base/models.py b/backend/base/models.py index 70a6f6e9..6370be93 100644 --- a/backend/base/models.py +++ b/backend/base/models.py @@ -13,7 +13,7 @@ # sys.maxsize throws psycopg2.errors.NumericValueOutOfRange: integer out of range # Set the max int manually -MAX_INT = 2 ** 31 - 1 +MAX_INT = 2**31 - 1 def _check_for_present_keys(instance, keys_iterable): @@ -217,7 +217,7 @@ class Meta: "date", name="garbage_collection_unique", violation_error_message="This type of garbage is already being collected on the same day for this " - "building.", + "building.", ), ] @@ -391,9 +391,9 @@ def clean(self): max_version_number = max(version_numbers) if ( - self.version_number == 0 - or self.version_number > max_version_number + 1 - or self.version_number in version_numbers + self.version_number == 0 + or self.version_number > max_version_number + 1 + or self.version_number in version_numbers ): self.version_number = max_version_number + 1 From 2fc54d3464cbf8678dca9ceb059d824b2ab23580 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 23:00:28 +0100 Subject: [PATCH 0259/1000] Added i18n to /signup --- frontend/locales/nl.json | 6 +++++- frontend/pages/signup.tsx | 15 +++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/locales/nl.json b/frontend/locales/nl.json index 7df1aa68..8739f6da 100644 --- a/frontend/locales/nl.json +++ b/frontend/locales/nl.json @@ -1,3 +1,7 @@ { - "Unable to log in with provided credentials.": "E-mail of wachtwoord is niet juist." + "Unable to log in with provided credentials.": "E-mailadres of wachtwoord is niet juist.", + "A user is already registered with this e-mail address.": "Er bestaat al een gebruiker met dit e-mailadres.", + "This password is too short. It must contain at least 8 characters.": "Dit wachtwoord is te kort. Een wachtwoord moet minimum 8 karakters lang zijn.", + "This password is too common.": "Dit wachtwoord is te eenvoudig.", + "This password is entirely numeric.": "Dit wachtwoord bestaat volledig uit cijfers, er moeten ook lettertekens in voorkomen." } \ No newline at end of file diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 688c1717..9a8fee0b 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -5,8 +5,10 @@ import styles from "@/styles/Login.module.css"; import Image from "next/image"; import fire from "@/public/fire_image.png"; import signup from "@/lib/signup"; +import {useTranslation} from "react-i18next"; export default function Signup() { + const { t } = useTranslation(); const router = useRouter(); const [firstname, setFirstname] = useState(""); const [lastname, setLastname] = useState(""); @@ -29,20 +31,21 @@ export default function Signup() { if (errorRes.status === 400) { let errors = []; if (errorRes.data.firstname) { - errors.push(errorRes.data.firstname); + errors.push(...errorRes.data.firstname); } if (errorRes.data.lastname) { - errors.push(errorRes.data.lastname); + errors.push(...errorRes.data.lastname); } if (errorRes.data.email) { - errors.push(errorRes.data.email); + errors.push(...errorRes.data.email); } if (errorRes.data.password1) { - errors.push(errorRes.data.password1); + errors.push(...errorRes.data.password1); } if (errorRes.data.password2) { - errors.push(errorRes.data.password2); + errors.push(...errorRes.data.password2); } + console.log(errors); setErrorMessages(errors); } else { console.error(err); @@ -74,7 +77,7 @@ export default function Signup() {
    {errorMessages.map((err, i) => (
  • - {err} + {t(err)}
  • ))}
From 71b086a5e8f189b7d2cb1cbbd0917f3860f26963 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 22:07:06 +0000 Subject: [PATCH 0260/1000] Auto formatted code --- frontend/pages/signup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 9a8fee0b..55e7f34e 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -5,7 +5,7 @@ import styles from "@/styles/Login.module.css"; import Image from "next/image"; import fire from "@/public/fire_image.png"; import signup from "@/lib/signup"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; export default function Signup() { const { t } = useTranslation(); From b177e4f629123a622ae4f3a7c898ee223d73d530 Mon Sep 17 00:00:00 2001 From: Tibo Stroo Date: Fri, 24 Mar 2023 23:24:18 +0100 Subject: [PATCH 0261/1000] POST, PATCH a region is now a list of id's (fixes #124) --- backend/users/user_utils.py | 28 ++++++++++++++++++++++++++++ backend/users/views.py | 30 +++++++++++++----------------- 2 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 backend/users/user_utils.py diff --git a/backend/users/user_utils.py b/backend/users/user_utils.py new file mode 100644 index 00000000..a2f07dba --- /dev/null +++ b/backend/users/user_utils.py @@ -0,0 +1,28 @@ +from django.db import IntegrityError +from rest_framework import status +from rest_framework.response import Response + + +def try_adding_region_to_user_instance(user_instance, region_value): + try: + user_instance.region.add(region_value) + except IntegrityError as e: + return Response(str(e.__cause__), status=status.HTTP_400_BAD_REQUEST) + + +def add_regions_to_user(user_instance, regions_raw): + if not regions_raw: + return + + try: + regions = eval(regions_raw) + if not type(regions) == list: + raise SyntaxError() + except SyntaxError: + return Response({"message": "Invalid syntax. Regions must be a list of id's"}, + status=status.HTTP_400_BAD_REQUEST) + + for region in regions: + + if r := try_adding_region_to_user_instance(user_instance, region): + return r diff --git a/backend/users/views.py b/backend/users/views.py index fad84411..16f8f232 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,8 +1,8 @@ -import json - +from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +from base.models import User from base.permissions import ( IsAdmin, IsSuperStudent, @@ -12,13 +12,14 @@ CanDeleteUser, CanCreateUser, ) -from base.models import User from base.serializers import UserSerializer +from users.user_utils import add_regions_to_user from util.request_response_util import * -from drf_spectacular.utils import extend_schema TRANSLATE = {"role": "role_id"} +DESCRIPTION = "For region, pass a list of id's. For example: [1, 2, 3]" + # In GET, you only get active users # Except when you explicitly pass a parameter 'include_inactive' to the body of the request and set it as true @@ -46,7 +47,7 @@ class DefaultUser(APIView): # In the future, we probably won't use POST this way anymore (if we work with the whitelist method) # However, an easy workaround would be to add a default value to password (in e.g. `clean`) # -> probably the easiest way - @extend_schema(responses={201: UserSerializer, 400: None}) + @extend_schema(description=DESCRIPTION, responses={201: UserSerializer, 400: None}) def post(self, request): """ Create a new user @@ -64,11 +65,9 @@ def post(self, request): # Now that we have an ID, we can look at the many-to-many relationship region - if "region" in data.keys(): - region_dict = json.loads(data["region"]) - for value in region_dict.values(): - if r := _try_adding_region_to_user_instance(user_instance, value): - return r + if r := add_regions_to_user(user_instance, data["region"]): + user_instance.delete() + return r serializer = UserSerializer(user_instance) return post_success(serializer) @@ -118,7 +117,7 @@ def delete(self, request, user_id): return delete_success() - @extend_schema(responses={200: UserSerializer, 400: None}) + @extend_schema(description=DESCRIPTION, responses={200: UserSerializer, 400: None}) def patch(self, request, user_id): """ Edit user with given id @@ -140,12 +139,9 @@ def patch(self, request, user_id): return r # Now that we have an ID, we can look at the many-to-many relationship region - if "region" in data.keys(): - region_dict = json.loads(data["region"]) - user_instance.region.clear() - for value in region_dict.values(): - if r := _try_adding_region_to_user_instance(user_instance, value): - return r + if r := add_regions_to_user(user_instance, data["region"]): + user_instance.delete() + return r serializer = UserSerializer(user_instance) return patch_success(serializer) From 3468506bb6a95cca806e6e0a270fda7390c7de8c Mon Sep 17 00:00:00 2001 From: TiboStr Date: Fri, 24 Mar 2023 22:27:16 +0000 Subject: [PATCH 0262/1000] Auto formatted code --- backend/users/user_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/users/user_utils.py b/backend/users/user_utils.py index a2f07dba..c9b1fe0d 100644 --- a/backend/users/user_utils.py +++ b/backend/users/user_utils.py @@ -19,10 +19,10 @@ def add_regions_to_user(user_instance, regions_raw): if not type(regions) == list: raise SyntaxError() except SyntaxError: - return Response({"message": "Invalid syntax. Regions must be a list of id's"}, - status=status.HTTP_400_BAD_REQUEST) + return Response( + {"message": "Invalid syntax. Regions must be a list of id's"}, status=status.HTTP_400_BAD_REQUEST + ) for region in regions: - if r := try_adding_region_to_user_instance(user_instance, region): return r From 7730a7ded437cc7ffe14c2f59bcda5a98386de58 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Fri, 24 Mar 2023 23:39:35 +0100 Subject: [PATCH 0263/1000] No more session cookies --- backend/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/settings.py b/backend/config/settings.py index bfc4dbe1..c4929ba5 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -57,7 +57,6 @@ CREATED_APPS = ["base"] - INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CREATED_APPS REST_FRAMEWORK = { @@ -81,6 +80,7 @@ AUTH_USER_MODEL = "base.User" REST_AUTH = { + "SESSION_LOGIN": False, "USE_JWT": True, "JWT_AUTH_HTTPONLY": True, "JWT_AUTH_SAMESITE": "Strict", From 3760d431f7057035e086da738e31ed474aff1645 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Fri, 24 Mar 2023 23:41:30 +0100 Subject: [PATCH 0264/1000] CustomLogoutView --- backend/authentication/urls.py | 4 ++-- backend/authentication/views.py | 33 ++++++++++++++------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 00634a3d..ad1b0a5a 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -9,7 +9,7 @@ from authentication.views import ( LoginViewWithHiddenTokens, RefreshViewHiddenTokens, - LogoutViewWithBlacklisting, CustomRegisterView, + CustomLogoutView, CustomRegisterView, ) urlpatterns = [ @@ -25,6 +25,6 @@ path("token/verify/", TokenVerifyView.as_view(), name="token_verify"), path("token/refresh/", RefreshViewHiddenTokens.as_view(), name="token_refresh"), # URLs that require a user to be logged in with a valid session / token. - path("logout/", LogoutViewWithBlacklisting.as_view(), name="rest_logout"), + path("logout/", CustomLogoutView.as_view(), name="rest_logout"), path("password/change/", PasswordChangeView.as_view(), name="rest_password_change"), ] diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 7c3a1f3a..4af39270 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -6,6 +6,7 @@ ) from dj_rest_auth.utils import jwt_encode from dj_rest_auth.views import LogoutView, LoginView +from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema from rest_framework import status @@ -25,6 +26,7 @@ class CustomRegisterView(APIView): serializer_class = CustomRegisterSerializer + @extend_schema(responses={200: None, 403: None}) def post(self, request): """ Register a new user @@ -35,20 +37,14 @@ def post(self, request): lobby_instances = Lobby.objects.filter(email=data.get('email')) if not lobby_instances: return Response( - { - "error": "Unauthorized signup", - "message": f"The given email address {data.get('email')} has no entry in the lobby. You must contact an admin to gain access to the platform." - }, + {"message": f"The given email address {data.get('email')} has no entry in the lobby. You must contact an admin to gain access to the platform."}, status=status.HTTP_403_FORBIDDEN ) lobby_instance = lobby_instances[0] # check if the verification code is valid if lobby_instance.verification_code != data.get('verification_code'): return Response( - { - "error": "Unauthorized signup", - "message": "Invalid verification code" - }, + {"message": "Invalid verification code"}, status=status.HTTP_403_FORBIDDEN ) @@ -72,39 +68,38 @@ def post(self, request): return response -class LogoutViewWithBlacklisting(LogoutView): +class CustomLogoutView(APIView): permission_classes = [IsAuthenticated] - serializer_class = CookieTokenRefreshSerializer @extend_schema(responses={200: None, 401: None, 500: None}) - def logout(self, request): + def post(self, request): response = Response( - {"detail": _("Successfully logged out.")}, + {"message": _("Successfully logged out.")}, status=status.HTTP_200_OK, ) cookie_name = getattr(settings, "JWT_AUTH_REFRESH_COOKIE", None) - - unset_jwt_cookies(response) - try: if cookie_name and cookie_name in request.COOKIES: token = RefreshToken(request.COOKIES.get(cookie_name)) token.blacklist() except KeyError: - response.data = {"detail": _("Refresh token was not included in request cookies.")} + response.data = {"message": _("Refresh token was not included in request cookies.")} response.status_code = status.HTTP_401_UNAUTHORIZED except (TokenError, AttributeError, TypeError) as error: if hasattr(error, "args"): if "Token is blacklisted" in error.args or "Token is invalid or expired" in error.args: - response.data = {"detail": _(error.args[0])} + response.data = {"message": _(error.args[0])} response.status_code = status.HTTP_401_UNAUTHORIZED else: - response.data = {"detail": _("An error has occurred.")} + response.data = {"message": _("An error has occurred.")} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR else: - response.data = {"detail": _("An error has occurred.")} + response.data = {"message": _("An error has occurred.")} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + + unset_jwt_cookies(response) + return response From 1aa5c79f254c7af95f2439087c8d9ce30d161806 Mon Sep 17 00:00:00 2001 From: Tibo Stroo <71405687+TiboStr@users.noreply.github.com> Date: Fri, 24 Mar 2023 23:42:31 +0100 Subject: [PATCH 0265/1000] Create dependabot.yml (fixes #130) --- .github/dependabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..fa5561bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + - package-ecosystem: "pip" + directory: "/backend" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + From 7c0f398333324e39fb7720797c628784d89f2fb3 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Sat, 25 Mar 2023 00:08:11 +0100 Subject: [PATCH 0266/1000] now loop over data in response --- frontend/pages/login.tsx | 7 +++++-- frontend/pages/signup.tsx | 18 +++--------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 4b1d8306..1c835bb8 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -33,11 +33,14 @@ export default function Login() { await router.push("/welcome"); }, (err) => { + let errors = []; let errorRes = err.response; if (errorRes.status === 400) { - if (errorRes.data.non_field_errors) { - setErrorMessages(errorRes.data.non_field_errors); + let data : [any, string[]][] = Object.entries(errorRes.data); + for (const [_, errorValues] of data) { + errors.push(...errorValues); } + setErrorMessages(errors); } else { console.error(err); } diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 9a8fee0b..5e9f4251 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -30,22 +30,10 @@ export default function Signup() { let errorRes = err.response; if (errorRes.status === 400) { let errors = []; - if (errorRes.data.firstname) { - errors.push(...errorRes.data.firstname); + let data : [any, string[]][] = Object.entries(errorRes.data); + for (const [_, errorValues] of data) { + errors.push(...errorValues); } - if (errorRes.data.lastname) { - errors.push(...errorRes.data.lastname); - } - if (errorRes.data.email) { - errors.push(...errorRes.data.email); - } - if (errorRes.data.password1) { - errors.push(...errorRes.data.password1); - } - if (errorRes.data.password2) { - errors.push(...errorRes.data.password2); - } - console.log(errors); setErrorMessages(errors); } else { console.error(err); From b14f6c0c88cd5625c068b56c22b164682b4f1c44 Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Fri, 24 Mar 2023 23:10:08 +0000 Subject: [PATCH 0267/1000] Auto formatted code --- frontend/pages/signup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/signup.tsx b/frontend/pages/signup.tsx index 976d3b7e..86dea0de 100644 --- a/frontend/pages/signup.tsx +++ b/frontend/pages/signup.tsx @@ -30,7 +30,7 @@ export default function Signup() { let errorRes = err.response; if (errorRes.status === 400) { let errors = []; - let data : [any, string[]][] = Object.entries(errorRes.data); + let data: [any, string[]][] = Object.entries(errorRes.data); for (const [_, errorValues] of data) { errors.push(...errorValues); } From 3f88702656c2c2ed97e2691aea4d58ba837fa5e7 Mon Sep 17 00:00:00 2001 From: jonathancasters Date: Sat, 25 Mar 2023 00:10:09 +0100 Subject: [PATCH 0268/1000] CustomLoginView --- backend/authentication/urls.py | 6 +++--- backend/authentication/views.py | 38 +++++++++++++++++---------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index ad1b0a5a..7ff98355 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -3,13 +3,13 @@ PasswordResetConfirmView, PasswordChangeView, ) -from django.urls import path, include +from django.urls import path from rest_framework_simplejwt.views import TokenVerifyView from authentication.views import ( LoginViewWithHiddenTokens, RefreshViewHiddenTokens, - CustomLogoutView, CustomRegisterView, + CustomLogoutView, CustomRegisterView, CustomLoginView, ) urlpatterns = [ @@ -21,7 +21,7 @@ PasswordResetConfirmView.as_view(), name="password_reset_confirm", ), - path("login/", LoginViewWithHiddenTokens.as_view(), name="rest_login"), + path("login/", CustomLoginView.as_view(), name="rest_login"), path("token/verify/", TokenVerifyView.as_view(), name="token_verify"), path("token/refresh/", RefreshViewHiddenTokens.as_view(), name="token_refresh"), # URLs that require a user to be logged in with a valid session / token. diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 4af39270..5f1546d6 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -5,8 +5,7 @@ set_jwt_refresh_cookie, set_jwt_cookies, ) from dj_rest_auth.utils import jwt_encode -from dj_rest_auth.views import LogoutView, LoginView -from django.core.exceptions import ObjectDoesNotExist +from dj_rest_auth.views import LoginView from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema from rest_framework import status @@ -19,6 +18,7 @@ from authentication.serializers import CustomRegisterSerializer from base.models import Lobby +from base.serializers import UserSerializer from config import settings from util.request_response_util import request_to_dict @@ -37,7 +37,8 @@ def post(self, request): lobby_instances = Lobby.objects.filter(email=data.get('email')) if not lobby_instances: return Response( - {"message": f"The given email address {data.get('email')} has no entry in the lobby. You must contact an admin to gain access to the platform."}, + { + "message": f"The given email address {data.get('email')} has no entry in the lobby. You must contact an admin to gain access to the platform."}, status=status.HTTP_403_FORBIDDEN ) lobby_instance = lobby_instances[0] @@ -74,7 +75,7 @@ class CustomLogoutView(APIView): @extend_schema(responses={200: None, 401: None, 500: None}) def post(self, request): response = Response( - {"message": _("Successfully logged out.")}, + {"message": _("successfully logged out")}, status=status.HTTP_200_OK, ) @@ -84,18 +85,18 @@ def post(self, request): token = RefreshToken(request.COOKIES.get(cookie_name)) token.blacklist() except KeyError: - response.data = {"message": _("Refresh token was not included in request cookies.")} + response.data = {"message": _("refresh token was not included in request cookies")} response.status_code = status.HTTP_401_UNAUTHORIZED except (TokenError, AttributeError, TypeError) as error: if hasattr(error, "args"): if "Token is blacklisted" in error.args or "Token is invalid or expired" in error.args: - response.data = {"message": _(error.args[0])} + response.data = {"message": _(error.args[0].lower())} response.status_code = status.HTTP_401_UNAUTHORIZED else: - response.data = {"message": _("An error has occurred.")} + response.data = {"message": _("an error has occurred.")} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR else: - response.data = {"message": _("An error has occurred.")} + response.data = {"message": _("an error has occurred.")} response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR unset_jwt_cookies(response) @@ -103,6 +104,17 @@ def post(self, request): return response +class CustomLoginView(LoginView): + def get_response(self): + data = { + "message": "successful login", + "user": UserSerializer(self.user).data, + } + response = Response(data, status=status.HTTP_200_OK) + set_jwt_cookies(response, self.access_token, self.refresh_token) + return response + + class RefreshViewHiddenTokens(TokenRefreshView): serializer_class = CookieTokenRefreshSerializer @@ -118,13 +130,3 @@ def finalize_response(self, request, response, *args, **kwargs): # we don't want this info to be in the body for security reasons (HTTPOnly!) del response.data["refresh"] return super().finalize_response(request, response, *args, **kwargs) - - -class LoginViewWithHiddenTokens(LoginView): - def finalize_response(self, request, response, *args, **kwargs): - if response.status_code == 200 and "access_token" in response.data: - response.data["access_token"] = _("set successfully") - if response.status_code == 200 and "refresh_token" in response.data: - response.data["refresh_token"] = _("set successfully") - - return super().finalize_response(request, response, *args, **kwargs) From 93ac528d6d101aa6bedf01ba90dbaa63f96571dc Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Sat, 25 Mar 2023 09:59:29 +0100 Subject: [PATCH 0269/1000] Removed alert when succesfully created account --- frontend/pages/login.tsx | 24 ++++++++++++++++-------- frontend/pages/signup.tsx | 22 ++++++++++++---------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/pages/login.tsx b/frontend/pages/login.tsx index 1c835bb8..ca1f1b0c 100644 --- a/frontend/pages/login.tsx +++ b/frontend/pages/login.tsx @@ -2,13 +2,13 @@ import BaseHeader from "@/components/header/BaseHeader"; import styles from "styles/Login.module.css"; import Image from "next/image"; import filler_image from "../public/filler_image.png"; -import { login, verifyToken } from "@/lib/login"; +import {login, verifyToken} from "@/lib/login"; import React, {FormEvent, useEffect, useState} from "react"; -import { useRouter } from "next/router"; -import { useTranslation } from 'react-i18next'; +import {useRouter} from "next/router"; +import {useTranslation} from 'react-i18next'; export default function Login() { - const { t } = useTranslation(); + const {t} = useTranslation(); const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -36,7 +36,7 @@ export default function Login() { let errors = []; let errorRes = err.response; if (errorRes.status === 400) { - let data : [any, string[]][] = Object.entries(errorRes.data); + let data: [any, string[]][] = Object.entries(errorRes.data); for (const [_, errorValues] of data) { errors.push(...errorValues); } @@ -50,23 +50,31 @@ export default function Login() { return ( <> - +
- My App Logo + My App Logo
- + Login.
+
+ Succes! Uw account werd met succes aangemaakt! + +
+
-
From 0342d73744863f87fa4e237dd2f0e144e3f21e8b Mon Sep 17 00:00:00 2001 From: simvadnbu Date: Sun, 26 Mar 2023 16:36:59 +0200 Subject: [PATCH 0310/1000] Added logout, signup & login component --- frontend/components/loginform.tsx | 156 ++++++++++++++++++++ frontend/components/logoutbutton.tsx | 31 ++++ frontend/components/signupform.tsx | 204 +++++++++++++++++++++++++++ frontend/lib/reroute.tsx | 6 + frontend/lib/storage.tsx | 8 ++ frontend/pages/admin/dashboard.tsx | 21 +-- frontend/pages/default/dashboard.tsx | 33 +---- frontend/pages/login.tsx | 173 ++--------------------- frontend/pages/signup.tsx | 200 +------------------------- frontend/pages/student/dashboard.tsx | 33 +---- frontend/pages/syndic/dashboard.tsx | 32 +---- 11 files changed, 439 insertions(+), 458 deletions(-) create mode 100644 frontend/components/loginform.tsx create mode 100644 frontend/components/logoutbutton.tsx create mode 100644 frontend/components/signupform.tsx create mode 100644 frontend/lib/storage.tsx diff --git a/frontend/components/loginform.tsx b/frontend/components/loginform.tsx new file mode 100644 index 00000000..b3564bc5 --- /dev/null +++ b/frontend/components/loginform.tsx @@ -0,0 +1,156 @@ +import Image from "next/image"; +import filler_image from "@/public/filler_image.png"; +import styles from "@/styles/Login.module.css"; +import React, {FormEvent, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {useRouter} from "next/router"; +import login from "@/lib/login"; +import setSessionStorage from "@/lib/storage"; +import {getRoleDirection} from "@/lib/reroute"; + +function LoginForm() { + const { t } = useTranslation(); + const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [errorMessages, setErrorMessages] = useState([]); + + const handleSubmit = async (event: FormEvent): Promise => { + event.preventDefault(); + login(username, password).then( + async (res) => { + const roleId = res.data.user.role + setSessionStorage(roleId, res.data.user.id); + const direction = getRoleDirection(roleId, "dashboard"); + await router.push(direction); + }, + (err) => { + let errors = []; + let errorRes = err.response; + if (errorRes.status === 400) { + let data: [any, string[]][] = Object.entries(errorRes.data); + for (const [_, errorValues] of data) { + errors.push(...errorValues); + } + setErrorMessages(errors); + } else { + console.error(err); + } + } + ); + }; + + return ( + + ); +} + +export default LoginForm; \ No newline at end of file diff --git a/frontend/components/logoutbutton.tsx b/frontend/components/logoutbutton.tsx new file mode 100644 index 00000000..f6964caa --- /dev/null +++ b/frontend/components/logoutbutton.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import styles from "@/styles/Welcome.module.css"; +import {logout} from "@/lib/logout"; +import {useRouter} from "next/router"; + +function LogoutButton() { + const router = useRouter(); + const handleLogout = () => { + logout().then( + async (res) => { + if (res.status === 200) { + sessionStorage.removeItem("id"); + sessionStorage.removeItem("role"); + await router.push("/login"); + } + }, + (err) => { + console.error(err); + } + ); + }; + + return ( + + ); +} + +export default LogoutButton; \ No newline at end of file diff --git a/frontend/components/signupform.tsx b/frontend/components/signupform.tsx new file mode 100644 index 00000000..c0392aeb --- /dev/null +++ b/frontend/components/signupform.tsx @@ -0,0 +1,204 @@ +import Image from "next/image"; +import fire from "@/public/fire_image.png"; +import styles from "@/styles/Login.module.css"; +import React, {FormEvent, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {useRouter} from "next/router"; +import signup from "@/lib/signup"; + +function SignupForm() { + const { t } = useTranslation(); + const router = useRouter(); + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); + const [email, setEmail] = useState(""); + const [password1, setPassword1] = useState(""); + const [password2, setPassword2] = useState(""); + const [errorMessages, setErrorMessages] = useState([]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setErrorMessages([]); + signup(firstname, lastname, email, password1, password2).then( + async (res) => { + if (res.status == 201) { + await router.push( + { + pathname: "/login", + query: { createdAccount: true }, + }, + "/login" + ); + } + }, + (err) => { + let errorRes = err.response; + if (errorRes.status === 400) { + let errors = []; + let data: [any, string[]][] = Object.entries(errorRes.data); + for (const [_, errorValues] of data) { + errors.push(...errorValues); + } + setErrorMessages(errors); + } else { + console.error(err); + } + } + ); + }; + + return ( +
+
+
+
+
+
+ My App Logo +
+
+
+
+
+ + Sign up. +
+ +
+
    + {errorMessages.map((err, i) => ( +
  • {t(err)}
  • + ))} +
+
+ +
+ + ) => { + setFirstname(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Voornaam is verplicht."); + }} + required + placeholder="Voornaam" + /> +
+ +
+ + ) => { + setLastname(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Achternaam is verplicht."); + }} + required + placeholder="Achternaam" + /> +
+ +
+ + ) => { + setEmail(e.target.value); + }} + required + placeholder="naam@voorbeeld.com" + /> +
+ +
+ + ) => { + setPassword1(e.target.value); + e.target.setCustomValidity(""); + }} + onInvalid={(e: React.ChangeEvent) => { + e.target.setCustomValidity("Wachtwoord is verplicht."); + }} + required + placeholder="Wachtwoord" + /> +
+ +
+ + ) => { + e.target.setCustomValidity(""); + setPassword2(e.target.value); + if (password1 !== e.target.value) { + e.target.setCustomValidity( + "Wachtwoorden zijn niet gelijk." + ); + } else { + e.target.setCustomValidity(""); + } + }} + onInvalid={(e: React.ChangeEvent) => { + if (password1 !== e.target.value) { + e.target.setCustomValidity( + "Wachtwoorden zijn niet gelijk." + ); + } else { + e.target.setCustomValidity(""); + } + }} + required + placeholder="Wachtwoord" + /> +
+ +
+ +
+ +

+ Heb je al een account? Ga naar login +

+ +
+
+
+
+
+
+
+ ); +} + +export default SignupForm; \ No newline at end of file diff --git a/frontend/lib/reroute.tsx b/frontend/lib/reroute.tsx index 16a58c71..612eb505 100644 --- a/frontend/lib/reroute.tsx +++ b/frontend/lib/reroute.tsx @@ -1,3 +1,4 @@ +import {getUserRole} from "@/lib/user_info"; export function getSpecificDirection(role: string, direction: string): string { @@ -9,4 +10,9 @@ export function getSpecificDirection(role: string, direction: string): string { return `${path}/${direction}`; } +export function getRoleDirection(roleId : string, direction : string) : string { + const role : string = getUserRole(roleId); + return getSpecificDirection(role, direction); +} + export default getSpecificDirection; \ No newline at end of file diff --git a/frontend/lib/storage.tsx b/frontend/lib/storage.tsx new file mode 100644 index 00000000..9cb37adf --- /dev/null +++ b/frontend/lib/storage.tsx @@ -0,0 +1,8 @@ +import {getUserRole} from "@/lib/user_info"; + +export default function setSessionStorage(roleId : string, userId : string) { + const role : string = getUserRole(roleId); + + sessionStorage.setItem("id", userId); + sessionStorage.setItem("role", role); +} \ No newline at end of file diff --git a/frontend/pages/admin/dashboard.tsx b/frontend/pages/admin/dashboard.tsx index b8533db9..0aa62df4 100644 --- a/frontend/pages/admin/dashboard.tsx +++ b/frontend/pages/admin/dashboard.tsx @@ -4,9 +4,9 @@ import soon from "public/coming_soon.png"; import Image from "next/image"; import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import { logout } from "@/lib/logout"; import { getAllUsers } from "@/lib/welcome"; import Loading from "@/components/loading"; +import LogoutButton from "@/components/logoutbutton"; export default function AdminDashboard() { const router = useRouter(); @@ -31,21 +31,6 @@ export default function AdminDashboard() { ); }, []); - const handleLogout = () => { - logout().then( - async (res) => { - if (res.status === 200) { - sessionStorage.removeItem("id"); - sessionStorage.removeItem("role"); - await router.push("/login"); - } - }, - (err) => { - console.error(err); - } - ); - }; - return ( <> @@ -56,9 +41,7 @@ export default function AdminDashboard() {

Welcome to the Admin Dashboard!

Site coming soon - +

Users: