From c14cd42ae93d270f3b47cbf8c0427b8ab87f88cf Mon Sep 17 00:00:00 2001 From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:37:14 -0800 Subject: [PATCH] Tracking pull request to merge release-1.64.0 to master (#2328) * Update package.json 1.64.0 * fix: 2274 - permissions (#2300) * set default permissions to AllowNone * Chore/2267 - remove commentator's name (#2320) * chore: removes get_serializer_context * -refactors mixin and updates the files that use it -adds extra check to credit transfers details page * chore: removes commented code * chore: adds mixin to sales submission list serializer so it passes test -------- Co-authored-by: tim738745 * feat: 2324 - add legacy column (#2340) * feat: 2266 - reassessments (#2334) * 2313 Endpoint testing (#2341) * chore: tests for user deetails and update * checks for is government before displaying supplier details page (idir view) * chore: organization tests tasks: some changes to organization viewset , adds permissions fix: adds check for is government on user viewset for download_active * chore: 2313 - small changes (#2342) * testing: credit requests (#2343) * fix: adds user id to basic mixin so comments can be edited chore: testing * removing extra lines * space * revert: 2324 - legacy column (#2350) * fix: 2351 - ICBC file upload error (#2352) * fix: updates credit agreement alert to use displayname (#2354) fix: grabs supplementary comments also using supplementary id instead of just reassessment id chore: adds empty strings to default value in user details form so there are no console errors if there are blank fields --------- Co-authored-by: tim738745 <98717409+tim738745@users.noreply.github.com> Co-authored-by: Emily <44536222+emi-hi@users.noreply.github.com> Co-authored-by: tim738745 --- .github/workflows/build-on-dev.yaml | 32 --- .../workflows/cleanup-cron-workflow-runs.yaml | 8 +- .github/workflows/cleanup-workflow-runs.yaml | 6 +- .github/workflows/dev-build.yaml | 116 --------- .github/workflows/post-prod-release.yaml | 42 ++- .github/workflows/pr-build-template.yaml | 239 ------------------ .github/workflows/release-build.yaml | 174 ------------- .github/workflows/unit-test-template.yaml | 78 ------ backend/.coveragerc | 4 + backend/.s2i/bin/assemble | 129 ---------- backend/.s2i/environment | 1 - backend/api/mixins/user_mixin.py | 49 ++++ backend/api/permissions/allow_none.py | 6 + backend/api/permissions/sales_forecast.py | 32 +-- backend/api/permissions/same_organization.py | 49 ++++ backend/api/serializers/credit_agreement.py | 34 +-- .../serializers/credit_agreement_comment.py | 19 +- backend/api/serializers/credit_transfer.py | 40 +-- .../serializers/credit_transfer_comment.py | 16 +- backend/api/serializers/model_year_report.py | 4 +- .../model_year_report_assessment.py | 2 +- .../model_year_report_assessment_comment.py | 19 +- .../model_year_report_confirmation.py | 15 +- .../serializers/model_year_report_history.py | 20 +- .../api/serializers/model_year_report_noa.py | 35 +-- .../model_year_report_supplemental.py | 52 ++-- backend/api/serializers/sales_submission.py | 31 +-- .../serializers/sales_submission_comment.py | 23 +- backend/api/serializers/user.py | 2 +- backend/api/services/icbc_upload.py | 8 +- backend/api/services/minio.py | 33 ++- backend/api/services/model_year_report.py | 63 ++--- backend/api/services/supplemental_report.py | 57 +++++ backend/api/tests/test_credit_requests.py | 101 +++++++- backend/api/tests/test_organizations.py | 227 ++++++++++++++++- backend/api/tests/test_users.py | 99 ++++++++ backend/api/urls.py | 4 +- backend/api/viewsets/compliance_ratio.py | 8 +- backend/api/viewsets/credit_agreement.py | 25 +- backend/api/viewsets/credit_request.py | 23 +- backend/api/viewsets/credit_transaction.py | 33 ++- backend/api/viewsets/credit_transfer.py | 21 +- backend/api/viewsets/dashboard.py | 15 +- backend/api/viewsets/icbc_verification.py | 48 ++-- backend/api/viewsets/model_year_report.py | 38 +-- ...model_year_report_compliance_obligation.py | 86 ++++--- .../model_year_report_consumer_sales.py | 17 +- backend/api/viewsets/notification.py | 20 +- backend/api/viewsets/organization.py | 49 ++-- backend/api/viewsets/role.py | 8 +- backend/api/viewsets/sales_forecast.py | 9 +- .../viewsets/signing_authority_assertion.py | 8 +- backend/api/viewsets/upload.py | 2 +- backend/api/viewsets/user.py | 33 ++- backend/api/viewsets/vehicle.py | 17 +- backend/auditable/views.py | 36 +-- backend/zeva/settings.py | 2 +- frontend/.s2i/bin/assemble | 130 ---------- frontend/package.json | 2 +- frontend/src/app/router.js | 3 + frontend/src/app/routes/Compliance.js | 10 +- frontend/src/app/routes/Organizations.js | 2 +- .../app/utilities/reconcileSupplementaries.js | 18 +- .../src/compliance/AssessmentContainer.js | 6 +- .../ComplianceReportSummaryContainer.js | 3 +- .../components/CreditAgreementsAlert.js | 6 +- .../components/CreditTransfersDetailsPage.js | 5 +- .../VehicleSupplierEditContainer.js | 6 +- .../supplementary/SupplementaryContainer.js | 70 +++-- .../src/users/components/UserDetailsForm.js | 14 +- openshift/templates/backend/backend-bc.yaml | 84 ------ openshift/templates/knp/1-base.yaml | 30 --- openshift/templates/knp/2-apps.yaml | 59 ----- openshift/templates/knp/3-spilo.yaml | 86 ------- openshift/templates/knp/README.md | 13 - openshift/templates/knp/Zeva-Git-Model.drawio | 1 - openshift/templates/knp/knp-diagram.drawio | 134 ---------- openshift/templates/knp/knp-quick-start.yaml | 60 ----- 78 files changed, 1118 insertions(+), 1991 deletions(-) delete mode 100644 .github/workflows/build-on-dev.yaml delete mode 100644 .github/workflows/dev-build.yaml delete mode 100644 .github/workflows/pr-build-template.yaml delete mode 100644 .github/workflows/release-build.yaml delete mode 100644 .github/workflows/unit-test-template.yaml delete mode 100755 backend/.s2i/bin/assemble delete mode 100644 backend/.s2i/environment create mode 100644 backend/api/mixins/user_mixin.py create mode 100644 backend/api/permissions/allow_none.py create mode 100644 backend/api/permissions/same_organization.py create mode 100644 backend/api/tests/test_users.py delete mode 100755 frontend/.s2i/bin/assemble delete mode 100644 openshift/templates/backend/backend-bc.yaml delete mode 100644 openshift/templates/knp/1-base.yaml delete mode 100644 openshift/templates/knp/2-apps.yaml delete mode 100644 openshift/templates/knp/3-spilo.yaml delete mode 100644 openshift/templates/knp/README.md delete mode 100644 openshift/templates/knp/Zeva-Git-Model.drawio delete mode 100644 openshift/templates/knp/knp-diagram.drawio delete mode 100644 openshift/templates/knp/knp-quick-start.yaml diff --git a/.github/workflows/build-on-dev.yaml b/.github/workflows/build-on-dev.yaml deleted file mode 100644 index 18fa6250e..000000000 --- a/.github/workflows/build-on-dev.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Please refer to ./readme.md for how to build single pull request - -# Update this workflow name per pull request -name: Build PR on Dev - -on: - pull_request: - types: [opened, edited, synchronize, reopened] - branches: - - 'release-*' -jobs: - - # call-unit-test: - # uses: ./.github/workflows/unit-test-template.yaml - # with: - # pr-number: ${{ github.event.pull_request.number }} - - call-pr-build: - if: endsWith( github.event.pull_request.title, 'build-on-dev' ) - # needs: call-unit-test - uses: ./.github/workflows/pr-build-template.yaml - with: - pr-number: ${{ github.event.pull_request.number }} - version: ${{ github.event.pull_request.base.ref }} - secrets: - tools-namespace: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-tools - dev-namespace: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-dev - zeva-dev-username: ${{ secrets.ZEVA_DEV_USERNAME }} - zeva-dev-password: ${{ secrets.ZEVA_DEV_PASSWORD }} - openshift-server: ${{ secrets.OPENSHIFT_SERVER }} - openshift-token: ${{ secrets.OPENSHIFT_TOKEN }} - openshiftLicensePlate: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} diff --git a/.github/workflows/cleanup-cron-workflow-runs.yaml b/.github/workflows/cleanup-cron-workflow-runs.yaml index a98d4b0b9..affa47088 100644 --- a/.github/workflows/cleanup-cron-workflow-runs.yaml +++ b/.github/workflows/cleanup-cron-workflow-runs.yaml @@ -1,7 +1,7 @@ -name: Scheduled cleanup old workflow runs +name: Cleanup old workflow runs (scheduled) on: schedule: - - cron: '0 0 * * 0' + - cron: "0 0 * * 0" # At 00:00 on Sunday. jobs: @@ -11,9 +11,9 @@ jobs: actions: write steps: - name: Delete workflow runs - uses: Mattraks/delete-workflow-runs@v2.0.4 + uses: Mattraks/delete-workflow-runs@v2.0.6 with: token: ${{ github.token }} repository: ${{ github.repository }} retain_days: 15 - keep_minimum_runs: 10 \ No newline at end of file + keep_minimum_runs: 10 diff --git a/.github/workflows/cleanup-workflow-runs.yaml b/.github/workflows/cleanup-workflow-runs.yaml index 13987355e..6928c987a 100644 --- a/.github/workflows/cleanup-workflow-runs.yaml +++ b/.github/workflows/cleanup-workflow-runs.yaml @@ -3,11 +3,11 @@ on: workflow_dispatch: inputs: days: - description: 'Number of days.' + description: "Number of days." required: true default: 15 minimum_runs: - description: 'The minimum runs to keep for each workflow.' + description: "The minimum runs to keep for each workflow." required: true default: 10 jobs: @@ -17,7 +17,7 @@ jobs: actions: write steps: - name: Delete workflow runs - uses: Mattraks/delete-workflow-runs@v2.0.4 + uses: Mattraks/delete-workflow-runs@v2.0.6 with: token: ${{ github.token }} repository: ${{ github.repository }} diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml deleted file mode 100644 index a1a766aa4..000000000 --- a/.github/workflows/dev-build.yaml +++ /dev/null @@ -1,116 +0,0 @@ -## For each release, please update the value of workflow name, branches and PR_NUMBER -## Also update frontend/package.json version - -name: Dev Build 1.63.0 - -on: - push: - branches: [release-1.63.0] - paths: - - frontend/** - - backend/** - workflow_dispatch: - workflow_call: - -env: - ## The pull request number of the Tracking pull request to merge the release branch to main - PR_NUMBER: 2305 - VERSION: 1.63.0 - GIT_URL: https://github.com/bcgov/zeva.git - TOOLS_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-tools - DEV_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-dev - TEST_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-test - PROD_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-prod - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - call-unit-test: - uses: ./.github/workflows/unit-test-template.yaml - with: - pr-number: 2305 - - build: - name: Build ZEVA on Openshift - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: call-unit-test - - steps: - - name: Check out repository - uses: actions/checkout@v3 - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} - openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} - insecure_skip_tls_verify: true - namespace: ${{ env.TOOLS_NAMESPACE }} - - - name: Build ZEVA Backend - run: | - cd openshift/templates/backend - oc process -f ./backend-bc.yaml NAME=zeva SUFFIX=-build-${{ env.PR_NUMBER }} VERSION=build-${{ env.VERSION }}-${{ env.PR_NUMBER }} GIT_URL=${{ env.GIT_URL }} GIT_REF=refs/pull/${{ env.PR_NUMBER }}/head | oc apply --wait=true -f - -n ${{ env.TOOLS_NAMESPACE }} - oc start-build --wait=true zeva-backend-build-${{ env.PR_NUMBER }} - - - name: Build ZEVA Frontend - run: | - cd openshift/templates/frontend - oc process -f ./frontend-bc-docker.yaml NAME=zeva SUFFIX=-build-${{ env.PR_NUMBER }} VERSION=build-${{ env.VERSION }}-${{ env.PR_NUMBER }} GIT_URL=${{ env.GIT_URL }} GIT_REF=refs/pull/${{ env.PR_NUMBER }}/head | oc apply --wait=true -f - -n ${{ env.TOOLS_NAMESPACE }} - oc start-build --wait=true zeva-frontend-build-${{ env.PR_NUMBER }} - - deploy-on-dev: - name: Deploy ZEVA on Dev - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: build - - steps: - - name: Check out repository - uses: actions/checkout@v3 - with: - ref: refs/pull/${{ env.PR_NUMBER }}/head - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} - openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} - insecure_skip_tls_verify: true - namespace: ${{ env.TOOLS_NAMESPACE }} - - - name: Tag Frontend Image from tools to dev - run: | - oc tag ${{ env.TOOLS_NAMESPACE }}/zeva-frontend:build-${{ env.VERSION }}-${{ env.PR_NUMBER }} ${{ env.DEV_NAMESPACE }}/zeva-frontend:dev-${{ env.VERSION }} - oc tag ${{ env.TOOLS_NAMESPACE }}/zeva-backend:build-${{ env.VERSION }}-${{ env.PR_NUMBER }} ${{ env.DEV_NAMESPACE }}/zeva-backend:dev-${{ env.VERSION }} - - # helm status will show an error if the helm release doesn't exist. The error will be ignored. - - name: Deply zeva-frontend on Dev - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-frontend - helm status -n ${{ env.DEV_NAMESPACE }} zeva-frontend-dev - if [ $? -eq 0 ]; then - echo "zeva-frontend-dev release exists already" - helm upgrade --set frontendImageTagname=dev-${{ env.VERSION }},openshiftLicensePlate=${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} -n ${{ env.DEV_NAMESPACE }} -f ./values-dev.yaml zeva-frontend-dev . - else - echo "zeva-frontend-dev release does not exist" - helm install --set frontendImageTagname=dev-${{ env.VERSION }},openshiftLicensePlate=${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} -n ${{ env.DEV_NAMESPACE }} -f ./values-dev.yaml zeva-frontend-dev . - fi - - # helm status will show an error if the helm release doesn't exist. The error will be ignored. - - name: Deply zeva-backend on Dev - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-backend - helm status -n ${{ env.DEV_NAMESPACE }} zeva-backend-dev - if [ $? -eq 0 ]; then - echo "zeva-backend-dev release exists already" - helm upgrade --set backendImageTagname=dev-${{ env.VERSION }} -n ${{ env.DEV_NAMESPACE }} -f ./values-dev.yaml zeva-backend-dev . - else - echo "zeva-backend-dev release does not exist" - helm install --set backendImageTagname=dev-${{ env.VERSION }} -n ${{ env.DEV_NAMESPACE }} -f ./values-dev.yaml zeva-backend-dev . - fi diff --git a/.github/workflows/post-prod-release.yaml b/.github/workflows/post-prod-release.yaml index c58437e05..fee4acc3c 100644 --- a/.github/workflows/post-prod-release.yaml +++ b/.github/workflows/post-prod-release.yaml @@ -9,7 +9,7 @@ on: env: GH_TOKEN: ${{ github.token }} - + jobs: verify-pr: name: Verify pull request title started with Tracking @@ -42,20 +42,21 @@ jobs: id: tag_name with: source: ${{ github.event.pull_request.head.ref }} - find: 'release-' - replace: 'v' + find: "release-" + replace: "v" + + - name: Checkout repository + uses: actions/checkout@v4.1.1 + with: + ref: master - - name: Create Release - uses: softprops/action-gh-release@v1.1.0 + - name: Github Release + uses: elgohr/Github-Release-Action@v5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - name: ${{ steps.tag_name.outputs.value }} - tag_name: ${{ steps.tag_name.outputs.value }} - target_commitish: master - body: | - ${{ github.event.pull_request.body }} - token: ${{ env.GITHUB_TOKEN }} - draft: false - prerelease: false + title: ${{ steps.tag_name.outputs.value }} + tag: ${{ steps.tag_name.outputs.value }} - name: Get Current Default Branch and Set Tag Name id: get_default_branch @@ -64,7 +65,7 @@ jobs: current_default_branch=$(gh api repos/${{ github.repository }} | jq -r '.default_branch') echo "Current default branch is: $current_default_branch" echo "current_default_branch=$current_default_branch" >> $GITHUB_OUTPUT - + # # Extract the current release version number (assumes format is release-X.Y.Z) # current_version=$(echo "$current_default_branch" | grep -oP '\d+\.\d+\.\d+') # echo "Current version extracted: $current_version" @@ -78,10 +79,10 @@ jobs: run: | # Get the current default branch from the previous step current_default_branch="${{ steps.get_default_branch.outputs.current_default_branch }}" - + # Extract the current release version number (assumes format is release-X.Y.Z) current_version=$(echo "$current_default_branch" | grep -oP '\d+\.\d+\.\d+') - + # Increment the minor version (X.Y.Z -> X.(Y+1).0) major_version=$(echo "$current_version" | cut -d. -f1) minor_version=$(echo "$current_version" | cut -d. -f2) @@ -90,21 +91,16 @@ jobs: # Increment the minor version by 1 for the new release branch new_minor_version=$((minor_version + 1)) new_release_branch="release-${major_version}.${new_minor_version}.0" - + echo "New release branch will be: $new_release_branch" echo "new_release_branch=$new_release_branch" >> $GITHUB_OUTPUT - - name: Checkout repository - uses: actions/checkout@v4.1.1 - with: - ref: master - - name: Create New Release Branch id: create_new_release_branch run: | # Get the new release branch name from the previous step new_release_branch="${{ steps.get_new_release_branch.outputs.new_release_branch }}" - + # Create the new branch from master git checkout -b $new_release_branch master git push origin $new_release_branch diff --git a/.github/workflows/pr-build-template.yaml b/.github/workflows/pr-build-template.yaml deleted file mode 100644 index e213f3a13..000000000 --- a/.github/workflows/pr-build-template.yaml +++ /dev/null @@ -1,239 +0,0 @@ -name: PR Build Template - -on: - workflow_call: - inputs: - pr-number: - required: true - type: string - version: - required: true - type: string - secrets: - tools-namespace: - required: true - dev-namespace: - required: true - zeva-dev-username: - required: true - zeva-dev-password: - required: true - openshift-server: - required: true - openshift-token: - required: true - openshiftLicensePlate: - required: true - - -env: - GIT_URL: https://github.com/bcgov/zeva.git - -jobs: - - database: - - name: Start Database - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - - name: Check out repository - uses: actions/checkout@v4.1.1 - with: - ref: refs/pull/${{ inputs.pr-number }}/head - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.openshift-server }} - openshift_token: ${{ secrets.openshift-token }} - insecure_skip_tls_verify: true - namespace: ${{ secrets.tools-namespace }} - - # This stpe will load the test data on PVC pr-dev-build-data into database - # - name: Setup Database - # shell: bash {0} - # run: | - # cd charts/zeva-spilo - # helm status -n ${{ secrets.dev-namespace }} zeva-spilo-dev-${{ inputs.pr-number }} - # if [ $? -eq 0 ]; then - # echo "zeva-spilo-dev-${{ inputs.pr-number }} exists already" - # else - # echo "==> Installing zeva-spilo-dev-${{ inputs.pr-number }}, will load the test data into database as well, it may take few minutes .." - # helm install -n ${{ secrets.dev-namespace }} -f ./values-dev-pr.yaml --wait zeva-spilo-dev-${{ inputs.pr-number }} . - # oc -n ${{ secrets.dev-namespace }} wait --for=condition=Ready pod/zeva-spilo-dev-${{ inputs.pr-number }}-0 - # echo "==> Spilo is up and running" - # echo "==> Mounting dataload PVC" - # oc -n ${{ secrets.dev-namespace }} set volume statefulset/zeva-spilo-dev-${{ inputs.pr-number }} --add --name=dataload -t pvc --claim-name=pr-dev-build-data --overwrite --mount-path=/dataload || true - # sleep 120 - # oc -n ${{ secrets.dev-namespace }} wait --for=condition=Ready pod/zeva-spilo-dev-${{ inputs.pr-number }}-0 - # echo "==> Spilo is up and running again" - # echo "==> Creating user and database and loading test data" - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "create user ${{ secrets.zeva-dev-username }} WITH PASSWORD '${{ secrets.zeva-dev-password }}'" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "create database zeva owner ${{ secrets.zeva-dev-username }} ENCODING 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8'" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_filename='postgresql-%H.log'" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_connections='off'" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_disconnections='off'" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_checkpoints='off'" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "select pg_reload_conf()" || true - # oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- bash -c "psql zeva < /dataload/LatestBuild.sql >> /tmp/zeva-spilo-dev-${{ inputs.pr-number }}-$(date +"%Y%m%d-%I%M%S%p").log 2>&1" - # echo "==> Spilo is ready for use" - # fi - - - name: Setup Database - shell: bash {0} - run: | - cd charts/zeva-spilo - helm status -n ${{ secrets.dev-namespace }} zeva-spilo-dev-${{ inputs.pr-number }} - if [ $? -eq 0 ]; then - echo "zeva-spilo-dev-${{ inputs.pr-number }} exists already" - else - helm upgrade --install -n ${{ secrets.dev-namespace }} -f ./values-dev-pr.yaml --wait zeva-spilo-dev-${{ inputs.pr-number }} . - oc -n ${{ secrets.dev-namespace }} wait --for=condition=Ready pod/zeva-spilo-dev-${{ inputs.pr-number }}-0 - echo "==> Spilo is up and running" - echo "==> Creating user and database and loading test data" - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "create user ${{ secrets.zeva-dev-username }} WITH PASSWORD '${{ secrets.zeva-dev-password }}'" || true - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "create database zeva owner ${{ secrets.zeva-dev-username }} ENCODING 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8'" || true - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_filename='postgresql-%H.log'" || true - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_connections='off'" || true - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_disconnections='off'" || true - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "ALTER SYSTEM SET log_checkpoints='off'" || true - oc -n ${{ secrets.dev-namespace }} exec zeva-spilo-dev-${{ inputs.pr-number }}-0 -- psql -c "select pg_reload_conf()" || true - echo "==> Spilo is ready for use" - fi - - build-frontend: - - name: Build ZEVA Frontend on Openshift - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: database - - steps: - - - name: Check out repository - uses: actions/checkout@v4.1.1 - with: - ref: refs/pull/${{ inputs.pr-number }}/head - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.openshift-server }} - openshift_token: ${{ secrets.openshift-token }} - insecure_skip_tls_verify: true - namespace: ${{ secrets.tools-namespace }} - - - name: Build ZEVA Frontend - run: | - cd openshift/templates/frontend - oc process -f ./frontend-bc-docker.yaml NAME=zeva SUFFIX=-build-${{ inputs.pr-number }} VERSION=build-${{ inputs.version }}-${{ inputs.pr-number }} GIT_URL=${{ env.GIT_URL }} GIT_REF=refs/pull/${{ inputs.pr-number }}/head | oc apply --wait=true -f - -n ${{ secrets.tools-namespace }} - oc start-build --wait=true zeva-frontend-build-${{ inputs.pr-number }} - - build-backend: - - name: Build ZEVA Backend on Openshift - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: database - - steps: - - - name: Check out repository - uses: actions/checkout@v4.1.1 - with: - ref: refs/pull/${{ inputs.pr-number }}/head - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.openshift-server }} - openshift_token: ${{ secrets.openshift-token }} - insecure_skip_tls_verify: true - namespace: ${{ secrets.tools-namespace }} - - - name: Build ZEVA Backend - run: | - cd openshift/templates/backend - oc process -f ./backend-bc.yaml NAME=zeva SUFFIX=-build-${{ inputs.pr-number }} VERSION=build-${{ inputs.version }}-${{ inputs.pr-number }} GIT_URL=${{ env.GIT_URL }} GIT_REF=refs/pull/${{ inputs.pr-number }}/head | oc apply --wait=true -f - -n ${{ secrets.tools-namespace }} - oc start-build --wait=true zeva-backend-build-${{ inputs.pr-number }} - - deploy-on-dev: - - name: Deploy ZEVA on Dev - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: [build-frontend,build-backend] - - steps: - - - name: Check out repository - uses: actions/checkout@v4.1.1 - with: - ref: refs/pull/${{ inputs.pr-number }}/head - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.openshift-server }} - openshift_token: ${{ secrets.openshift-token }} - insecure_skip_tls_verify: true - namespace: ${{ secrets.tools-namespace }} - - - name: Tag Frontend Image from tools to dev - run: | - oc tag ${{ secrets.tools-namespace }}/zeva-frontend:build-${{ inputs.version }}-${{ inputs.pr-number }} ${{ secrets.dev-namespace }}/zeva-frontend:dev-${{ inputs.version }}-${{ inputs.pr-number }} - oc tag ${{ secrets.tools-namespace }}/zeva-backend:build-${{ inputs.version }}-${{ inputs.pr-number }} ${{ secrets.dev-namespace }}/zeva-backend:dev-${{ inputs.version }}-${{ inputs.pr-number }} - - - name: Deply zeva-frontend on Dev - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-frontend - helm status -n ${{ secrets.dev-namespace }} zeva-frontend-dev-${{ inputs.pr-number }} - if [ $? -eq 0 ]; then - echo "zeva-frontend-dev-${{ inputs.pr-number }} release exists already" - helm upgrade --set frontendImageTagname=dev-${{ inputs.version }}-${{ inputs.pr-number }} \ - --set openshiftLicensePlate=${{ secrets.openshiftLicensePlate }} \ - --set suffix=-dev-${{ inputs.pr-number }} \ - --set frontendConfigMap.apiBase=https://zeva-backend-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca/api \ - --set frontendConfigMap.keycloakCallbackUrl=https://zeva-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca\ - --set frontendConfigMap.keycloakPostLogoutUrl=https://zeva-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca\ - --set appConfigMap.backendHostName=zeva-backend-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - --set appConfigMap.databaseServiceName=zeva-spilo-dev-${{ inputs.pr-number }} \ - --set appConfigMap.hostName=zeva-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - -n ${{ secrets.dev-namespace }} -f ./values-dev.yaml zeva-frontend-dev-${{ inputs.pr-number }} . - else - echo "zeva-frontend-dev-${{ inputs.pr-number }} release does not exist" - helm install --set frontendImageTagname=dev-${{ inputs.version }}-${{ inputs.pr-number }} \ - --set openshiftLicensePlate=${{ secrets.openshiftLicensePlate }} \ - --set suffix=-dev-${{ inputs.pr-number }} \ - --set frontendConfigMap.apiBase=https://zeva-backend-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca/api \ - --set frontendConfigMap.keycloakCallbackUrl=https://zeva-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - --set frontendConfigMap.keycloakPostLogoutUrl=https://zeva-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - --set appConfigMap.backendHostName=zeva-backend-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - --set appConfigMap.databaseServiceName=zeva-spilo-dev-${{ inputs.pr-number }} \ - --set appConfigMap.hostName=zeva-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - -n ${{ secrets.dev-namespace }} -f ./values-dev.yaml zeva-frontend-dev-${{ inputs.pr-number }} . - fi - - - name: Deply zeva-backend on Dev - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-backend - helm status -n ${{ secrets.dev-namespace }} zeva-backend - if [ $? -eq 0 ]; then - echo "zeva-backend-dev-${{ inputs.pr-number }} release exists already" - helm upgrade --set backendImageTagname=dev-${{ inputs.version }}-${{ inputs.pr-number }} \ - --set suffix=-dev-${{ inputs.pr-number }} \ - --set backendRoute.hostName=zeva-backend-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - -n ${{ secrets.dev-namespace }} -f ./values-dev.yaml zeva-backend-dev-${{ inputs.pr-number }} . - else - echo "zeva-backend-dev-${{ inputs.pr-number }} release does not exist" - helm install --set backendImageTagname=dev-${{ inputs.version }}-${{ inputs.pr-number }} \ - --set suffix=-dev-${{ inputs.pr-number }} \ - --set backendRoute.hostName=zeva-backend-dev-${{ inputs.pr-number }}.apps.silver.devops.gov.bc.ca \ - -n ${{ secrets.dev-namespace }} -f ./values-dev.yaml zeva-backend-dev-${{ inputs.pr-number }} . - fi - \ No newline at end of file diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml deleted file mode 100644 index 21ca74cae..000000000 --- a/.github/workflows/release-build.yaml +++ /dev/null @@ -1,174 +0,0 @@ -## For each release, please update the value of workflow name, branches and PR_NUMBER -## Also update frontend/package.json version - -name: Release Build 1.63.0 - -on: - workflow_dispatch: - workflow_call: - -env: - ## The pull request number of the Tracking pull request to merge the release branch to main - PR_NUMBER: 2305 - VERSION: 1.63.0 - GIT_URL: https://github.com/bcgov/zeva.git - TOOLS_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-tools - DEV_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-dev - TEST_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-test - PROD_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-prod - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - call-unit-test: - uses: ./.github/workflows/unit-test-template.yaml - with: - pr-number: 2305 - - build: - name: Build ZEVA on Openshift - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: call-unit-test - - steps: - - name: Check out repository - uses: actions/checkout@v3 - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} - openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} - insecure_skip_tls_verify: true - namespace: ${{ env.TOOLS_NAMESPACE }} - - - name: Build ZEVA Backend - run: | - cd openshift/templates/backend - oc process -f ./backend-bc.yaml NAME=zeva SUFFIX=-build-${{ env.PR_NUMBER }} VERSION=build-${{ env.VERSION }}-${{ env.PR_NUMBER }} GIT_URL=${{ env.GIT_URL }} GIT_REF=refs/pull/${{ env.PR_NUMBER }}/head | oc apply --wait=true -f - -n ${{ env.TOOLS_NAMESPACE }} - oc start-build --wait=true zeva-backend-build-${{ env.PR_NUMBER }} - - - name: Build ZEVA Frontend - run: | - cd openshift/templates/frontend - oc process -f ./frontend-bc-docker.yaml NAME=zeva SUFFIX=-build-${{ env.PR_NUMBER }} VERSION=build-${{ env.VERSION }}-${{ env.PR_NUMBER }} GIT_URL=${{ env.GIT_URL }} GIT_REF=refs/pull/${{ env.PR_NUMBER }}/head | oc apply --wait=true -f - -n ${{ env.TOOLS_NAMESPACE }} - oc start-build --wait=true zeva-frontend-build-${{ env.PR_NUMBER }} - - deploy-on-test: - name: Deploy ZEVA on Test - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: build - - steps: - - name: Check out repository - uses: actions/checkout@v3 - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} - openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} - insecure_skip_tls_verify: true - namespace: ${{ env.TOOLS_NAMESPACE }} - - - name: Ask for approval for ZEVA Test deployment - uses: trstringer/manual-approval@v1.6.0 - with: - secret: ${{ github.TOKEN }} - approvers: emi-hi,tim738745,kuanfandevops,JulianForeman - minimum-approvals: 1 - issue-title: "ZEVA ${{ env.VERSION }} Test Deployment" - - - name: Tag Frontend Image from tools to Test - run: | - oc tag ${{ env.TOOLS_NAMESPACE }}/zeva-frontend:build-${{ env.VERSION }}-${{ env.PR_NUMBER }} ${{ env.TEST_NAMESPACE }}/zeva-frontend:test-${{ env.VERSION }} - oc tag ${{ env.TOOLS_NAMESPACE }}/zeva-backend:build-${{ env.VERSION }}-${{ env.PR_NUMBER }} ${{ env.TEST_NAMESPACE }}/zeva-backend:test-${{ env.VERSION }} - - # helm status will show an error if the helm release doesn't exist. The error will be ignored. - - name: Deply zeva-frontend on Test - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-frontend - helm status -n ${{ env.TEST_NAMESPACE }} zeva-frontend-test - if [ $? -eq 0 ]; then - echo "zeva-frontend-test release exists already" - helm upgrade --set frontendImageTagname=test-${{ env.VERSION }},openshiftLicensePlate=${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} -n ${{ env.TEST_NAMESPACE }} -f ./values-test.yaml zeva-frontend-test . - else - echo "zeva-frontend-test release does not exist" - helm install --set frontendImageTagname=test-${{ env.VERSION }},openshiftLicensePlate=${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} -n ${{ env.TEST_NAMESPACE }} -f ./values-test.yaml zeva-frontend-test . - fi - - # helm status will show an error if the helm release doesn't exist. The error will be ignored. - - name: Deply zeva-backend on Test - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-backend - helm status -n ${{ env.TEST_NAMESPACE }} zeva-backend-test - if [ $? -eq 0 ]; then - echo "zeva-backend-test release exists already" - helm upgrade --set backendImageTagname=test-${{ env.VERSION }} -n ${{ env.TEST_NAMESPACE }} -f ./values-test.yaml zeva-backend-test . - else - echo "zeva-backend-test release does not exist" - helm install --set backendImageTagname=test-${{ env.VERSION }} -n ${{ env.TEST_NAMESPACE }} -f ./values-test.yaml zeva-backend-test . - fi - - deploy-on-prod: - name: Deploy ZEVA on Prod - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: deploy-on-test - - steps: - - name: Check out repository - uses: actions/checkout@v3 - - - name: Log in to Openshift - uses: redhat-actions/oc-login@v1.3 - with: - openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} - openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} - insecure_skip_tls_verify: true - namespace: ${{ env.TOOLS_NAMESPACE }} - - - name: Ask for approval for ZEVA Prod deployment - uses: trstringer/manual-approval@v1.6.0 - with: - secret: ${{ github.TOKEN }} - approvers: kuanfandevops,tim738745,emi-hi,JulianForeman - minimum-approvals: 2 - issue-title: "ZEVA ${{ env.VERSION }} Prod Deployment" - - - name: Tag Frontend Image from tools to Prod - run: | - oc tag ${{ env.TOOLS_NAMESPACE }}/zeva-frontend:build-${{ env.VERSION }}-${{ env.PR_NUMBER }} ${{ env.PROD_NAMESPACE }}/zeva-frontend:prod-${{ env.VERSION }} - oc tag ${{ env.TOOLS_NAMESPACE }}/zeva-backend:build-${{ env.VERSION }}-${{ env.PR_NUMBER }} ${{ env.PROD_NAMESPACE }}/zeva-backend:prod-${{ env.VERSION }} - - - name: Deply zeva-frontend on Prod - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-frontend - helm status -n ${{ env.PROD_NAMESPACE }} zeva-frontend-prod - if [ $? -eq 0 ]; then - echo "zeva-frontend-prod release exists already" - helm upgrade --set frontendImageTagname=prod-${{ env.VERSION }},openshiftLicensePlate=${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} -n ${{ env.PROD_NAMESPACE }} -f ./values-prod.yaml zeva-frontend-prod . - else - echo "zeva-frontend-prod release does not exist" - helm install --set frontendImageTagname=prod-${{ env.VERSION }},openshiftLicensePlate=${{ secrets.OPENSHIFT_NAMESPACE_PLATE }} -n ${{ env.PROD_NAMESPACE }} -f ./values-prod.yaml zeva-frontend-prod . - fi - - - name: Deply zeva-backend on Prod - shell: bash {0} - run: | - cd charts/zeva-apps/charts/zeva-backend - helm status -n ${{ env.PROD_NAMESPACE }} zeva-backend-prod - if [ $? -eq 0 ]; then - echo "zeva-backend-prod release exists already" - helm upgrade --set backendImageTagname=prod-${{ env.VERSION }} -n ${{ env.PROD_NAMESPACE }} -f ./values-prod.yaml zeva-backend-prod . - else - echo "zeva-backend-prod release does not exist" - helm install --set backendImageTagname=prod-${{ env.VERSION }} -n ${{ env.PROD_NAMESPACE }} -f ./values-prod.yaml zeva-backend-prod . - fi diff --git a/.github/workflows/unit-test-template.yaml b/.github/workflows/unit-test-template.yaml deleted file mode 100644 index 4a8ebb3e6..000000000 --- a/.github/workflows/unit-test-template.yaml +++ /dev/null @@ -1,78 +0,0 @@ -name: Unit Test Template - -on: - workflow_call: - inputs: - pr-number: - required: true - type: string - -jobs: - - frontend-unit-test: - - name: Run Frontend Unit Tests - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - ref: refs/pull/${{ inputs.pr-number }}/head - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Frontend Jest coverage report - uses: ArtiomTr/jest-coverage-report-action@v2.2.1 - continue-on-error: false - with: - working-directory: frontend - output: report-markdown - prnumber: ${{ inputs.pr-number }} - test-script: npm run test - - backend-unit-test: - - name: Run Backend Unit Tests - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - ref: refs/pull/${{ inputs.pr-number }}/head - - - name: Remove all only keep backend files - run: | - mv backend .. - rm -rf * - mv ../backend/* . - pwd - ls -la - - # the DATABASE_* env variables have to have same value in as the action in order to connect to postgresql - # todo for itvr-django-test, create a new working-directory argument - - name: Run coverage report for django tests - uses: kuanfandevops/django-test-action@itvr-django-test - continue-on-error: false - env: - MINIO_ENDPOINT: minio:9000 - MINIO_ACCESS_KEY: testtest - MINIO_SECRET_KEY: testtest - DATABASE_NAME: testdb - DATABASE_USER: test - DATABASE_PASSWORD: test123 - DATABASE_ENGINE: postgresql - # DATABASE_SERVICE_NAME: 127.0.0.1 - # POSTGRESQL_SERVICE_HOST: 127.0.0.1 - # POSTGRESQL_SERVICE_PORT: 5432 - with: - settings-dir-path: zeva - requirements-file: requirements.txt - managepy-dir: ./ diff --git a/backend/.coveragerc b/backend/.coveragerc index 332f61a97..52495ca3b 100644 --- a/backend/.coveragerc +++ b/backend/.coveragerc @@ -15,5 +15,9 @@ exclude_lines = omit = zeva/wsgi.py api/management/* + */tests/* + */migrations/* + */models/* + */fixtures/* fail_under = 80 diff --git a/backend/.s2i/bin/assemble b/backend/.s2i/bin/assemble deleted file mode 100755 index ae8d905f8..000000000 --- a/backend/.s2i/bin/assemble +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_collectstatic() { - is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] -} - -function virtualenv_bin() { - # New versions of Python (>3.6) should use venv module - # from stdlib instead of virtualenv package - python3.9 -m venv $1 -} - -# Install pipenv or micropipenv to the separate virtualenv to isolate it -# from system Python packages and packages in the main -# virtualenv. Executable is simlinked into ~/.local/bin -# to be accessible. This approach is inspired by pipsi -# (pip script installer). -function install_tool() { - echo "---> Installing $1 packaging tool ..." - VENV_DIR=$HOME/.local/venvs/$1 - virtualenv_bin "$VENV_DIR" - # First, try to install the tool without --isolated which means that if you - # have your own PyPI mirror, it will take it from there. If this try fails, try it - # again with --isolated which ignores external pip settings (env vars, config file) - # and installs the tool from PyPI (needs internet connetion). - # $1$2 combines package name with [extras] or version specifier if is defined as $2``` - if ! $VENV_DIR/bin/pip install -U $1$2; then - echo "WARNING: Installation of $1 failed, trying again from official PyPI with pip --isolated install" - $VENV_DIR/bin/pip install --isolated -U $1$2 # Combines package name with [extras] or version specifier if is defined as $2``` - fi - mkdir -p $HOME/.local/bin - ln -s $VENV_DIR/bin/$1 $HOME/.local/bin/$1 -} - -set -e - -# First of all, check that we don't have disallowed combination of ENVs -if [[ ! -z "$ENABLE_PIPENV" && ! -z "$ENABLE_MICROPIPENV" ]]; then - echo "ERROR: Pipenv and micropipenv cannot be enabled at the same time!" - # podman/buildah does not relay this exit code but it will be fixed hopefuly - # https://github.com/containers/buildah/issues/2305 - exit 3 -fi - -shopt -s dotglob -echo "---> Installing application source ..." -mv /tmp/src/* "$HOME" - -# set permissions for any installed artifacts -fix-permissions /opt/app-root -P - - -if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then - echo "---> Upgrading pip, setuptools and wheel to latest version ..." - if ! pip install -U pip setuptools wheel; then - echo "WARNING: Installation of the latest pip, setuptools and wheel failed, trying again from official PyPI with pip --isolated install" - pip install --isolated -U pip setuptools wheel - fi -fi - -if [[ ! -z "$ENABLE_PIPENV" ]]; then - if [[ ! -z "$PIN_PIPENV_VERSION" ]]; then - # Add == as a prefix to pipenv version, if defined - PIN_PIPENV_VERSION="==$PIN_PIPENV_VERSION" - fi - install_tool "pipenv" "$PIN_PIPENV_VERSION" - echo "---> Installing dependencies via pipenv ..." - if [[ -f Pipfile ]]; then - pipenv install --deploy - elif [[ -f requirements.txt ]]; then - pipenv install -r requirements.txt - fi - # pipenv check -elif [[ ! -z "$ENABLE_MICROPIPENV" ]]; then - install_tool "micropipenv" "[toml]" - echo "---> Installing dependencies via micropipenv ..." - # micropipenv detects Pipfile.lock and requirements.txt in this order - micropipenv install --deploy -elif [[ -f requirements.txt ]]; then - if [[ -z "${ARTIFACTORY_USER}" ]]; then - echo "---> Installing dependencies from external repo ..." - pip install -r requirements.txt - else - echo "---> Installing dependencies from artifactory ..." - # pip install -i https://$ARTIFACTORY_USER:$ARTIFACTORY_PASSWORD@artifacts.developer.gov.bc.ca/artifactory/api/pypi/pypi-remote/simple -r requirements.txt - pip install -r requirements.txt - fi -fi - -if [[ -f setup.py && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then - echo "---> Installing application ..." - pip install . -fi - -if should_collectstatic; then - ( - echo "---> Collecting Django static files ..." - - APP_HOME=$(readlink -f "${APP_HOME:-.}") - # Change the working directory to APP_HOME - PYTHONPATH="$(pwd)${PYTHONPATH:+:$PYTHONPATH}" - cd "$APP_HOME" - - # Look for 'manage.py' in the current directory - manage_file=./manage.py - - if [[ ! -f "$manage_file" ]]; then - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "'manage.py collectstatic' ignored." - exit - fi - - if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then - echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" - echo " $ python $manage_file collectstatic --noinput" - echo "Ignore this warning if you're not serving static files with Django." - exit - fi - - python $manage_file collectstatic --noinput - ) -fi - -# set permissions for any installed artifacts -fix-permissions /opt/app-root -P \ No newline at end of file diff --git a/backend/.s2i/environment b/backend/.s2i/environment deleted file mode 100644 index 18f3b0467..000000000 --- a/backend/.s2i/environment +++ /dev/null @@ -1 +0,0 @@ -DISABLE_MIGRATE=1 diff --git a/backend/api/mixins/user_mixin.py b/backend/api/mixins/user_mixin.py new file mode 100644 index 000000000..44081039a --- /dev/null +++ b/backend/api/mixins/user_mixin.py @@ -0,0 +1,49 @@ +from rest_framework.serializers import ModelSerializer, SerializerMethodField +from api.models.user_profile import UserProfile +from api.serializers.user import UserBasicSerializer +from api.serializers.organization import OrganizationNameSerializer + + +def get_user_data(username, request): + if username is None: + return f"{username} does not exist on the object." + user_profile = UserProfile.objects.filter(username=username).first() + if not user_profile: + return { + "display_name": username + } # Return the username if the user profile doesn't exist + + is_government = False + if request is not None: + is_government = request.user.is_government + + if not user_profile.is_government or is_government: + # If the user is non-government or the requesting user is government, return full data + serializer = UserBasicSerializer(user_profile, read_only=True) + return serializer.data + else: + # If the requesting user is non-government and the user is government, limit info + organization = OrganizationNameSerializer( + user_profile.organization, read_only=True + ) + return { + "display_name": "Government User", + "is_government": user_profile.is_government, + "organization": organization.data, + } + + +class UserSerializerMixin(ModelSerializer): + create_user = SerializerMethodField() + + update_user = SerializerMethodField() + + def get_create_user(self, obj): + username = getattr(obj, "create_user", None) + request = self.context.get("request") + return get_user_data(username, request) + + def get_update_user(self, obj): + username = getattr(obj, "update_user", None) + request = self.context.get("request") + return get_user_data(username, request) diff --git a/backend/api/permissions/allow_none.py b/backend/api/permissions/allow_none.py new file mode 100644 index 000000000..b0611800e --- /dev/null +++ b/backend/api/permissions/allow_none.py @@ -0,0 +1,6 @@ +from rest_framework import permissions + + +class AllowNone(permissions.BasePermission): + def has_permission(self, request, view): + return False diff --git a/backend/api/permissions/sales_forecast.py b/backend/api/permissions/sales_forecast.py index 4e7bd1235..bd1715532 100644 --- a/backend/api/permissions/sales_forecast.py +++ b/backend/api/permissions/sales_forecast.py @@ -6,30 +6,20 @@ class SalesForecastPermissions(permissions.BasePermission): def has_permission(self, request, view): - model_year_report_id = view.kwargs.get("pk") - model_year_report = None - user = request.user - is_government = user.is_government - organization_matches = False - if model_year_report_id is not None: - model_year_report = get_model_year_report(model_year_report_id) - if ( - model_year_report is not None - and model_year_report.organization == user.organization - ): - organization_matches = True + user_is_government = request.user.is_government - if view.action == "save" and not is_government and organization_matches: + if view.action == "save" and not user_is_government: return True elif view.action == "records" or view.action == "totals": - if model_year_report is not None: - if is_government and model_year_report.validation_status not in [ - ModelYearReportStatuses.DRAFT, - ModelYearReportStatuses.DELETED, - ]: - return True - elif not is_government and organization_matches is True: - return True + if not user_is_government: + return True + model_year_report_id = view.kwargs.get("pk") + model_year_report = get_model_year_report(model_year_report_id) + if model_year_report.validation_status not in [ + ModelYearReportStatuses.DRAFT, + ModelYearReportStatuses.DELETED, + ]: + return True elif view.action == "template_url": return True diff --git a/backend/api/permissions/same_organization.py b/backend/api/permissions/same_organization.py new file mode 100644 index 000000000..90842df9e --- /dev/null +++ b/backend/api/permissions/same_organization.py @@ -0,0 +1,49 @@ +from rest_framework import permissions +from django.core.exceptions import ImproperlyConfigured + + +class SameOrganizationPermissions(permissions.BasePermission): + def has_permission(self, request, view): + if not hasattr(view, "same_org_permissions_context"): + raise ImproperlyConfigured( + """ + A view must have a "same_org_permissions_context" attribute if it uses "SameOrganizationPermissions" + """ + ) + permissions_context = view.same_org_permissions_context + + object_id = view.kwargs.get("pk") + user = request.user + if object_id is None or user.is_government: + return True + + actions_not_to_check = permissions_context.get("actions_not_to_check", []) + action = view.action + if action in actions_not_to_check: + return True + + user_org = user.organization + custom_pk_actions = permissions_context.get("custom_pk_actions", {}) + manager = None + path_to_org = None + if action in custom_pk_actions: + manager = custom_pk_actions[action]["manager"] + path_to_org = custom_pk_actions[action]["path_to_org"] + else: + manager = permissions_context["default_manager"] + path_to_org = permissions_context["default_path_to_org"] + + object = None + if path_to_org: + related_path = "__".join(path_to_org) + object = manager.select_related(related_path).filter(id=object_id).first() + else: + object = manager.filter(id=object_id).first() + if object is not None: + for step in path_to_org: + object = getattr(object, step) + object_org = object + if user_org != object_org: + return False + + return True diff --git a/backend/api/serializers/credit_agreement.py b/backend/api/serializers/credit_agreement.py index a56a37a86..8564cd2e8 100644 --- a/backend/api/serializers/credit_agreement.py +++ b/backend/api/serializers/credit_agreement.py @@ -9,8 +9,6 @@ from api.models.credit_agreement_transaction_types import CreditAgreementTransactionTypes from api.models.credit_class import CreditClass from api.models.model_year import ModelYear -from api.models.user_profile import UserProfile -from api.serializers.user import MemberSerializer from api.serializers.credit_agreement_attachment import CreditAgreementAttachmentSerializer from api.serializers.credit_agreement_comment import CreditAgreementCommentSerializer from api.serializers.credit_agreement_content import \ @@ -18,20 +16,9 @@ from .organization import OrganizationSerializer from api.models.credit_agreement_history import CreditAgreementHistory from api.services.minio import minio_remove_object +from api.mixins.user_mixin import UserSerializerMixin - -class CreditAgreementBaseSerializer: - def get_update_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.update_user) - - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - return serializer.data - - return obj.update_user - - -class CreditAgreementSerializer(ModelSerializer, CreditAgreementBaseSerializer): +class CreditAgreementSerializer(UserSerializerMixin): organization = OrganizationSerializer(read_only=True) transaction_type = EnumField(CreditAgreementTransactionTypes) credit_agreement_content = CreditAgreementContentSerializer( @@ -40,25 +27,16 @@ class CreditAgreementSerializer(ModelSerializer, CreditAgreementBaseSerializer): status = EnumField(CreditAgreementStatuses) comments = SerializerMethodField() attachments = SerializerMethodField() - update_user = SerializerMethodField() - - def get_update_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.update_user) - - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - return serializer.data - - return obj.update_user def get_comments(self, obj): + request = self.context.get('request') agreement_comment = CreditAgreementComment.objects.filter( credit_agreement=obj ).order_by('-create_timestamp') if agreement_comment.exists(): serializer = CreditAgreementCommentSerializer( - agreement_comment, read_only=True, many=True + agreement_comment, read_only=True, many=True, context={"request": request} ) return serializer.data @@ -287,15 +265,13 @@ class Meta: class CreditAgreementListSerializer( - ModelSerializer, EnumSupportSerializerMixin, CreditAgreementBaseSerializer + UserSerializerMixin, EnumSupportSerializerMixin ): - organization = OrganizationSerializer() credit_agreement_content = CreditAgreementContentSerializer( many=True, read_only=True ) status = EnumField(CreditAgreementStatuses) - update_user = SerializerMethodField() transaction_type = EnumField(CreditAgreementTransactionTypes) class Meta: diff --git a/backend/api/serializers/credit_agreement_comment.py b/backend/api/serializers/credit_agreement_comment.py index 2087338c3..fd89e2e16 100644 --- a/backend/api/serializers/credit_agreement_comment.py +++ b/backend/api/serializers/credit_agreement_comment.py @@ -1,24 +1,11 @@ -from rest_framework.serializers import ModelSerializer, \ - SerializerMethodField from api.models.credit_agreement_comment import CreditAgreementComment -from api.models.user_profile import UserProfile -from api.serializers.user import MemberSerializer, UserSerializer +from api.mixins.user_mixin import UserSerializerMixin - -class CreditAgreementCommentSerializer(ModelSerializer): +class CreditAgreementCommentSerializer(UserSerializerMixin): """ Serializer for credit agreement comments """ - create_user = SerializerMethodField() - - def get_create_user(self, obj): - user = UserProfile.objects.filter(username=obj.create_user).first() - if user is None: - return obj.create_user - - serializer = MemberSerializer(user, read_only=True) - return serializer.data - + class Meta: model = CreditAgreementComment fields = ( diff --git a/backend/api/serializers/credit_transfer.py b/backend/api/serializers/credit_transfer.py index f39250941..33d6c3ab8 100644 --- a/backend/api/serializers/credit_transfer.py +++ b/backend/api/serializers/credit_transfer.py @@ -18,29 +18,12 @@ CreditTransferCommentSerializer from api.serializers.credit_transfer_content import \ CreditTransferContentSerializer, CreditTransferContentSaveSerializer -from api.serializers.user import UserBasicSerializer from api.serializers.organization import OrganizationNameSerializer, OrganizationSerializer from api.services.credit_transaction import calculate_insufficient_credits from api.services.send_email import notifications_credit_transfers +from api.mixins.user_mixin import UserSerializerMixin - -class CreditTransferBaseSerializer: - def get_update_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.update_user) - - if user_profile.exists(): - serializer = UserBasicSerializer(user_profile.first(), read_only=True) - return serializer.data - - return obj.update_user - - def get_create_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.create_user) - if user_profile.exists(): - serializer = UserBasicSerializer(user_profile.first(), read_only=True) - return serializer.data - return obj.create_user - +class CreditTransferBaseSerializer(UserSerializerMixin): def get_history(self, obj): request = self.context.get('request') if request.user.is_government: @@ -66,22 +49,21 @@ def get_history(self, obj): class CreditTransferHistorySerializer( - ModelSerializer, EnumSupportSerializerMixin, - CreditTransferBaseSerializer + CreditTransferBaseSerializer, + EnumSupportSerializerMixin, ): - create_user = SerializerMethodField() - update_user = SerializerMethodField() status = EnumField(CreditTransferStatuses) comment = SerializerMethodField() def get_comment(self, obj): + request = self.context.get('request') credit_transfer_comment = CreditTransferComment.objects.filter( credit_transfer_history=obj ).first() if credit_transfer_comment: serializer = CreditTransferCommentSerializer( - credit_transfer_comment, read_only=True, many=False + credit_transfer_comment, read_only=True, many=False, context={'request': request} ) return serializer.data return None @@ -95,8 +77,8 @@ class Meta: class CreditTransferListSerializer( - ModelSerializer, EnumSupportSerializerMixin, - CreditTransferBaseSerializer + CreditTransferBaseSerializer, + EnumSupportSerializerMixin ): history = SerializerMethodField() credit_to = OrganizationNameSerializer() @@ -105,7 +87,6 @@ class CreditTransferListSerializer( ) debit_from = OrganizationNameSerializer() status = SerializerMethodField() - update_user = SerializerMethodField() def get_status(self, obj): request = self.context.get('request') @@ -126,8 +107,8 @@ class Meta: class CreditTransferSerializer( - ModelSerializer, EnumSupportSerializerMixin, - CreditTransferBaseSerializer + CreditTransferBaseSerializer, + EnumSupportSerializerMixin, ): history = SerializerMethodField() credit_to = OrganizationNameSerializer() @@ -136,7 +117,6 @@ class CreditTransferSerializer( ) debit_from = OrganizationNameSerializer() status = SerializerMethodField() - update_user = SerializerMethodField() sufficient_credits = SerializerMethodField() pending = SerializerMethodField() diff --git a/backend/api/serializers/credit_transfer_comment.py b/backend/api/serializers/credit_transfer_comment.py index 55a3b6f01..ab8885f30 100644 --- a/backend/api/serializers/credit_transfer_comment.py +++ b/backend/api/serializers/credit_transfer_comment.py @@ -1,26 +1,14 @@ """ Credit Transfer Comment Serializer """ -from rest_framework.serializers import ModelSerializer, SerializerMethodField -from api.models.user_profile import UserProfile from api.models.credit_transfer_comment import CreditTransferComment -from api.serializers.user import MemberSerializer +from api.mixins.user_mixin import UserSerializerMixin - -class CreditTransferCommentSerializer(ModelSerializer): +class CreditTransferCommentSerializer(UserSerializerMixin): """ Serializer for Credit Transfer comments """ - create_user = SerializerMethodField() - - def get_create_user(self, obj): - user = UserProfile.objects.filter(username=obj.create_user).first() - if user is None: - return obj.create_user - - serializer = MemberSerializer(user, read_only=True) - return serializer.data class Meta: model = CreditTransferComment diff --git a/backend/api/serializers/model_year_report.py b/backend/api/serializers/model_year_report.py index b0994543d..e7deb77b3 100644 --- a/backend/api/serializers/model_year_report.py +++ b/backend/api/serializers/model_year_report.py @@ -164,7 +164,7 @@ def get_makes(self, obj): def get_statuses(self, obj): request = self.context.get("request") - return get_model_year_report_statuses(obj, request.user) + return get_model_year_report_statuses(obj, request) def get_model_year_report_history(self, obj): request = self.context.get("request") @@ -192,7 +192,7 @@ def get_model_year_report_history(self, obj): create_user__in=users, ) - serializer = ModelYearReportHistorySerializer(history, many=True) + serializer = ModelYearReportHistorySerializer(history, many=True, context={"request": request}) return serializer.data diff --git a/backend/api/serializers/model_year_report_assessment.py b/backend/api/serializers/model_year_report_assessment.py index 5bbb31e93..f822b57ab 100644 --- a/backend/api/serializers/model_year_report_assessment.py +++ b/backend/api/serializers/model_year_report_assessment.py @@ -158,7 +158,7 @@ def get_assessment_comment(self, obj): if not assessment_comment: return [] serializer = ModelYearReportAssessmentCommentSerializer( - assessment_comment, read_only=True, many=True + assessment_comment, read_only=True, many=True, context={'request': request} ) return serializer.data diff --git a/backend/api/serializers/model_year_report_assessment_comment.py b/backend/api/serializers/model_year_report_assessment_comment.py index 378e8231c..75f4c841b 100644 --- a/backend/api/serializers/model_year_report_assessment_comment.py +++ b/backend/api/serializers/model_year_report_assessment_comment.py @@ -1,24 +1,11 @@ -from rest_framework.serializers import ModelSerializer, \ - SerializerMethodField from api.models.model_year_report_assessment_comment import ModelYearReportAssessmentComment -from api.models.user_profile import UserProfile -from api.serializers.user import MemberSerializer +from api.mixins.user_mixin import UserSerializerMixin - -class ModelYearReportAssessmentCommentSerializer(ModelSerializer): +class ModelYearReportAssessmentCommentSerializer(UserSerializerMixin): """ Serializer for assessment comments """ - create_user = SerializerMethodField() - - def get_create_user(self, obj): - user = UserProfile.objects.filter(username=obj.create_user).first() - if user is None: - return obj.create_user - - serializer = MemberSerializer(user, read_only=True) - return serializer.data - + class Meta: model = ModelYearReportAssessmentComment fields = ( diff --git a/backend/api/serializers/model_year_report_confirmation.py b/backend/api/serializers/model_year_report_confirmation.py index 2378de2a4..2f4a360d3 100644 --- a/backend/api/serializers/model_year_report_confirmation.py +++ b/backend/api/serializers/model_year_report_confirmation.py @@ -1,24 +1,13 @@ -from rest_framework.serializers import ModelSerializer, SerializerMethodField - from api.models.model_year_report_confirmation import \ ModelYearReportConfirmation -from api.models.user_profile import UserProfile -from api.serializers.user import UserSerializer from api.serializers.signing_authority_assertion import \ SigningAuthorityAssertionSerializer +from api.mixins.user_mixin import UserSerializerMixin -class ModelYearReportConfirmationSerializer(ModelSerializer): - create_user = SerializerMethodField() +class ModelYearReportConfirmationSerializer(UserSerializerMixin): signing_authority_assertion = SigningAuthorityAssertionSerializer() - def get_create_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.create_user) - if user_profile.exists(): - serializer = UserSerializer(user_profile.first(), read_only=True) - return serializer.data - - return obj.create_user class Meta: model = ModelYearReportConfirmation diff --git a/backend/api/serializers/model_year_report_history.py b/backend/api/serializers/model_year_report_history.py index d1fc40069..af85ac859 100644 --- a/backend/api/serializers/model_year_report_history.py +++ b/backend/api/serializers/model_year_report_history.py @@ -1,25 +1,13 @@ from enumfields.drf import EnumField -from rest_framework.serializers import ModelSerializer, SerializerMethodField - from api.models.model_year_report_history import ModelYearReportHistory from api.models.model_year_report_statuses import ModelYearReportStatuses -from api.models.user_profile import UserProfile -from api.serializers.user import MemberSerializer - +from api.mixins.user_mixin import UserSerializerMixin -class ModelYearReportHistorySerializer(ModelSerializer): - create_user = SerializerMethodField() +class ModelYearReportHistorySerializer(UserSerializerMixin): + validation_status = EnumField(ModelYearReportStatuses, read_only=True) - def get_create_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.create_user) - - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - return serializer.data - - return obj.create_user - + class Meta: model = ModelYearReportHistory fields = ('create_timestamp', 'create_user', 'validation_status') diff --git a/backend/api/serializers/model_year_report_noa.py b/backend/api/serializers/model_year_report_noa.py index 42dce717a..fe6c23849 100644 --- a/backend/api/serializers/model_year_report_noa.py +++ b/backend/api/serializers/model_year_report_noa.py @@ -6,9 +6,9 @@ from api.models.supplemental_report_history import SupplementalReportHistory from api.models.supplemental_report import SupplementalReport from api.models.user_profile import UserProfile -from api.serializers.user import MemberSerializer from django.db.models import Q from api.utilities.report_history import exclude_from_history +from api.mixins.user_mixin import UserSerializerMixin class ModelYearReportNoaSerializer(ModelSerializer): @@ -27,21 +27,14 @@ class Meta: ) -class SupplementalNOASerializer(ModelSerializer): +class SupplementalNOASerializer(UserSerializerMixin): status = SerializerMethodField() - update_user = SerializerMethodField() is_reassessment = SerializerMethodField() display_superseded_text = SerializerMethodField() def get_status(self, obj): return obj.validation_status.value - def get_update_user(self, obj): - user = UserProfile.objects.filter(username=obj.update_user).first() - if user is None: - return obj.create_user - return user.display_name - def get_display_superseded_text(self, obj): if obj.validation_status == ModelYearReportStatuses.ASSESSED: model_year_report_id = SupplementalReport.objects.filter( @@ -71,14 +64,13 @@ def get_is_reassessment(self, obj): class Meta: model = SupplementalReportHistory fields = ( - 'update_timestamp', 'status', 'id', 'update_user', 'supplemental_report_id', 'display_superseded_text', - 'is_reassessment' + 'update_timestamp', 'status', 'id', 'supplemental_report_id', 'display_superseded_text', + 'is_reassessment', 'update_user' ) -class ModelYearReportHistorySerializer(ModelSerializer): +class ModelYearReportHistorySerializer(UserSerializerMixin): status = SerializerMethodField() - create_user = SerializerMethodField() is_reassessment = SerializerMethodField() def get_is_reassessment(self, obj): @@ -91,15 +83,6 @@ def get_is_reassessment(self, obj): def get_status(self, obj): return obj.validation_status.value - def get_create_user(self, obj): - user = UserProfile.objects.filter(username=obj.create_user).first() - if user is None: - return None - - serializer = MemberSerializer(user) - - return serializer.data - class Meta: model = ModelYearReportHistory fields = ( @@ -113,7 +96,7 @@ class Meta: fields = ModelYearReportHistorySerializer.Meta.fields + ('supplemental_report_id',) -class SupplementalReportSerializer(ModelSerializer): +class SupplementalReportSerializer(UserSerializerMixin): status = SerializerMethodField() history = SerializerMethodField() is_supplementary = SerializerMethodField() @@ -145,7 +128,7 @@ def get_history(self, obj): refined_history = exclude_from_history(history, request.user) - serializer = SupplementalReportHistorySerializer(refined_history, many=True) + serializer = SupplementalReportHistorySerializer(refined_history, many=True, context={'request': request}) return serializer.data class Meta: @@ -156,7 +139,7 @@ class Meta: ) -class SupplementalModelYearReportSerializer(ModelSerializer): +class SupplementalModelYearReportSerializer(UserSerializerMixin): status = SerializerMethodField() history = SerializerMethodField() supplemental_id = SerializerMethodField() @@ -186,7 +169,7 @@ def get_history(self, obj): refined_history = exclude_from_history(history, request.user) - serializer = ModelYearReportHistorySerializer(refined_history, many=True) + serializer = ModelYearReportHistorySerializer(refined_history, many=True, context={'request': request}) return serializer.data diff --git a/backend/api/serializers/model_year_report_supplemental.py b/backend/api/serializers/model_year_report_supplemental.py index 56e8f7d5f..8229e9a35 100644 --- a/backend/api/serializers/model_year_report_supplemental.py +++ b/backend/api/serializers/model_year_report_supplemental.py @@ -1,8 +1,8 @@ from django.core.exceptions import ImproperlyConfigured +from django.db.models import Q from enumfields.drf import EnumField from rest_framework.serializers import ModelSerializer, \ SerializerMethodField, SlugRelatedField - from api.models.supplemental_report import SupplementalReport from api.models.supplemental_report_sales import SupplementalReportSales from api.models.model_year_report_address import ModelYearReportAddress @@ -27,12 +27,12 @@ SupplementalReportComment from api.services.minio import minio_get_object from api.models.user_profile import UserProfile -from api.serializers.user import MemberSerializer from api.models.supplemental_report_supplier_information import \ SupplementalReportSupplierInformation +from api.mixins.user_mixin import UserSerializerMixin +class ModelYearReportZevSalesSerializer(UserSerializerMixin): -class ModelYearReportZevSalesSerializer(ModelSerializer): class Meta: model = SupplementalReportSales fields = '__all__' @@ -40,6 +40,17 @@ class Meta: class ModelYearReportSupplementalCreditActivitySerializer(ModelSerializer): model_year = ModelYearSerializer() + category = SerializerMethodField() + + def get_category(self, obj): + context = getattr(self, "context", None) + if context is not None and context.get("category_transforms") is not None: + category_transforms = context.get("category_transforms") + category = obj.category + new_category = category_transforms.get(category) + if new_category is not None: + return new_category + return obj.category class Meta: model = SupplementalReportCreditActivity @@ -49,7 +60,8 @@ class Meta: ) -class ModelYearReportSupplementalCommentSerializer(ModelSerializer): +class ModelYearReportSupplementalCommentSerializer(UserSerializerMixin): + class Meta: model = SupplementalReportComment fields = ( @@ -60,19 +72,10 @@ class Meta: ) -class SupplementalReportAssessmentCommentSerializer(ModelSerializer): +class SupplementalReportAssessmentCommentSerializer(UserSerializerMixin): """ Serializer for supplemental report assessment comments """ - create_user = SerializerMethodField() - - def get_create_user(self, obj): - user = UserProfile.objects.filter(username=obj.create_user).first() - if user is None: - return obj.create_user - - serializer = MemberSerializer(user, read_only=True) - return serializer.data class Meta: model = SupplementalReportAssessmentComment @@ -201,7 +204,7 @@ def get_assessment_comment(self, obj): if not assessment_comment: return [] serializer = SupplementalReportAssessmentCommentSerializer( - assessment_comment, read_only=True, many=True + assessment_comment, read_only=True, many=True, context={'request': request} ) return serializer.data @@ -213,7 +216,7 @@ class Meta: ) -class ModelYearReportSupplementalSerializer(ModelSerializer): +class ModelYearReportSupplementalSerializer(UserSerializerMixin): status = EnumField(ModelYearReportStatuses) credit_activity = SerializerMethodField() supplier_information = SerializerMethodField() @@ -222,7 +225,6 @@ class ModelYearReportSupplementalSerializer(ModelSerializer): attachments = SerializerMethodField() from_supplier_comments = SerializerMethodField() actual_status = SerializerMethodField() - create_user = SerializerMethodField() reassessment = SerializerMethodField() def get_reassessment(self, obj): @@ -279,13 +281,6 @@ def get_reassessment(self, obj): 'status': obj.status.value } - def get_create_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.create_user) - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - return serializer.data - return obj.create_user - def get_actual_status(self, obj): request = self.context.get('request') # is this a reassessment report? if so this is the actual status @@ -321,10 +316,10 @@ def get_actual_status(self, obj): return model_year_report.validation_status.value def get_from_supplier_comments(self, obj): + comments = SupplementalReportComment.objects.filter( - supplemental_report_id=obj.id - ).order_by('-create_timestamp') - + Q(supplemental_report_id=obj.id) | Q(supplemental_report_id=obj.supplemental_id) + ).order_by('-create_timestamp') if comments.exists(): serializer = ModelYearReportSupplementalCommentSerializer(comments, many=True) return serializer.data @@ -353,6 +348,7 @@ def get_supplier_information(self, obj): return serializer.data def get_zev_sales(self, obj): + request = self.context.get('request') sales_queryset = SupplementalReportSales.objects.filter( supplemental_report_id=obj.id ) @@ -362,7 +358,7 @@ def get_zev_sales(self, obj): # supplemental_report_id=obj.supplemental_id # ) - sales_serializer = ModelYearReportZevSalesSerializer(sales_queryset, many=True) + sales_serializer = ModelYearReportZevSalesSerializer(sales_queryset, many=True, context={'request': request}) return sales_serializer.data diff --git a/backend/api/serializers/sales_submission.py b/backend/api/serializers/sales_submission.py index ad5b9720b..08987eaef 100644 --- a/backend/api/serializers/sales_submission.py +++ b/backend/api/serializers/sales_submission.py @@ -20,9 +20,7 @@ from api.models.icbc_snapshot_data import IcbcSnapshotData from api.serializers.sales_submission_comment import \ SalesSubmissionCommentSerializer -from api.models.user_profile import UserProfile from api.models.vin_statuses import VINStatuses -from api.serializers.user import MemberSerializer from api.serializers.organization import OrganizationSerializer from api.serializers.sales_submission_content import \ SalesSubmissionContentSerializer @@ -31,25 +29,9 @@ from api.models.sales_evidence import SalesEvidence from api.serializers.sales_evidence import SalesEvidenceSerializer from api.services.minio import minio_remove_object - +from api.mixins.user_mixin import UserSerializerMixin class BaseSerializer(): - def get_update_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.update_user) - - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - return serializer.data - - return obj.update_user - - def get_create_user(self, obj): - user_profile = UserProfile.objects.filter(username=obj.create_user) - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - return serializer.data - return obj.create_user - def get_validation_status(self, obj): request = self.context.get('request') @@ -68,10 +50,8 @@ def get_validation_status(self, obj): class SalesSubmissionHistorySerializer( - ModelSerializer, EnumSupportSerializerMixin, BaseSerializer + UserSerializerMixin, EnumSupportSerializerMixin, BaseSerializer ): - create_user = SerializerMethodField() - update_user = SerializerMethodField() validation_status = SerializerMethodField() class Meta: @@ -226,10 +206,9 @@ class Meta: class SalesSubmissionListSerializer( - SalesSubmissionBaseListSerializer + UserSerializerMixin, SalesSubmissionBaseListSerializer ): organization = OrganizationSerializer(read_only=True) - update_user = SerializerMethodField() class Meta(SalesSubmissionBaseListSerializer.Meta): fields = SalesSubmissionBaseListSerializer.Meta.fields + [ @@ -289,18 +268,16 @@ class Meta: class SalesSubmissionSerializer( - ModelSerializer, EnumSupportSerializerMixin, + UserSerializerMixin, EnumSupportSerializerMixin, BaseSerializer ): evidence = SerializerMethodField() content = SerializerMethodField() - create_user = SerializerMethodField() eligible = SerializerMethodField() history = SerializerMethodField() icbc_current_to = SerializerMethodField() organization = OrganizationSerializer(read_only=True) sales_submission_comment = SerializerMethodField() - update_user = SerializerMethodField() validation_status = SerializerMethodField() def get_evidence(self, instance): diff --git a/backend/api/serializers/sales_submission_comment.py b/backend/api/serializers/sales_submission_comment.py index 43b6d783c..9cc0c8569 100644 --- a/backend/api/serializers/sales_submission_comment.py +++ b/backend/api/serializers/sales_submission_comment.py @@ -1,32 +1,15 @@ """ Sales Submission Comment Serializer """ -from rest_framework.serializers import ModelSerializer, SerializerMethodField -from api.models.user_profile import UserProfile from api.models.sales_submission_comment import SalesSubmissionComment -from api.serializers.user import MemberSerializer +from api.mixins.user_mixin import UserSerializerMixin -class SalesSubmissionCommentSerializer(ModelSerializer): + +class SalesSubmissionCommentSerializer(UserSerializerMixin): """ Serializer for sales submission comments """ - create_user = SerializerMethodField() - - def get_create_user(self, obj): - request = self.context.get('request') - commenting_user = UserProfile.objects.filter(username=obj.create_user).first() - if commenting_user is None: - return obj.create_user - if not commenting_user.is_government or request.user.is_government: - ## if the commentor is not government or the request - # user is government, show all the data - serializer = MemberSerializer(commenting_user, read_only=True) - return serializer.data - else: - #if the request user is not government and the commenter - #is government - return {'display_name': 'Government User'} def update(self, instance, validated_data): instance.comment = validated_data.get("comment") diff --git a/backend/api/serializers/user.py b/backend/api/serializers/user.py index 01e69a717..e51899c23 100644 --- a/backend/api/serializers/user.py +++ b/backend/api/serializers/user.py @@ -38,7 +38,7 @@ class UserBasicSerializer(serializers.ModelSerializer): class Meta: model = UserProfile fields = ( - 'display_name', 'organization', 'is_government' + 'id', 'display_name', 'organization', 'is_government' ) class UserSerializer(serializers.ModelSerializer): diff --git a/backend/api/services/icbc_upload.py b/backend/api/services/icbc_upload.py index d8373ab3b..1bfdde5c5 100644 --- a/backend/api/services/icbc_upload.py +++ b/backend/api/services/icbc_upload.py @@ -41,7 +41,7 @@ def format_dataframe(df): @transaction.atomic -def ingest_icbc_spreadsheet(excelfile, requesting_user, dateCurrentTo, previous_excelfile): +def ingest_icbc_spreadsheet(current_excelfile, current_excelfile_name, requesting_user, dateCurrentTo, previous_excelfile): try: start_time = time.time() @@ -71,7 +71,7 @@ def ingest_icbc_spreadsheet(excelfile, requesting_user, dateCurrentTo, previous_ # Latest file processing df_l = [] for df in pd.read_csv( - excelfile, sep=",", error_bad_lines=False, iterator=True, low_memory=True, + current_excelfile, sep=",", error_bad_lines=False, iterator=True, low_memory=True, chunksize=50000, header=0 ): # df = format_dataframe(df) # pre-processing manually for now @@ -95,7 +95,7 @@ def ingest_icbc_spreadsheet(excelfile, requesting_user, dateCurrentTo, previous_ # latest filename if c_result.empty: print("No file changes detected.") - current_to_date.filename = excelfile + current_to_date.filename = current_excelfile_name current_to_date.save() return (True, 0, 0) @@ -210,7 +210,7 @@ def ingest_icbc_spreadsheet(excelfile, requesting_user, dateCurrentTo, previous_ has completed. If the upload failed then the IcbcUploadDate object will have an empty filename which we can skip on next upload """ - current_to_date.filename = excelfile + current_to_date.filename = current_excelfile_name current_to_date.save() print("Total processing time: ", time.time() - start_time) diff --git a/backend/api/services/minio.py b/backend/api/services/minio.py index c49145f6e..62f81fcad 100644 --- a/backend/api/services/minio.py +++ b/backend/api/services/minio.py @@ -1,14 +1,15 @@ from datetime import timedelta from minio import Minio - from zeva.settings import MINIO -minio = Minio( - MINIO['ENDPOINT'], - access_key=MINIO['ACCESS_KEY'], - secret_key=MINIO['SECRET_KEY'], - secure=MINIO['USE_SSL'] -) + +def get_minio_client(): + return Minio( + MINIO['ENDPOINT'], + access_key=MINIO['ACCESS_KEY'], + secret_key=MINIO['SECRET_KEY'], + secure=MINIO['USE_SSL'] + ) def get_refined_object_name(object_name): @@ -19,7 +20,8 @@ def get_refined_object_name(object_name): def minio_get_object(object_name, response_headers=None): - return minio.presigned_get_object( + client = get_minio_client() + return client.presigned_get_object( bucket_name=MINIO['BUCKET_NAME'], object_name=get_refined_object_name(object_name), expires=timedelta(seconds=3600), @@ -27,8 +29,15 @@ def minio_get_object(object_name, response_headers=None): ) +def get_minio_object(object_name): + client = get_minio_client() + refined_object_name = get_refined_object_name(object_name) + return client.get_object(MINIO['BUCKET_NAME'], refined_object_name) + + def minio_put_object(object_name): - return minio.presigned_put_object( + client = get_minio_client() + return client.presigned_put_object( bucket_name=MINIO['BUCKET_NAME'], object_name=get_refined_object_name(object_name), expires=MINIO['EXPIRY'] @@ -36,14 +45,16 @@ def minio_put_object(object_name): def minio_remove_objects(objects_iter): - return minio.remove_objects( + client = get_minio_client() + return client.remove_objects( bucket_name=MINIO['BUCKET_NAME'], objects_iter=objects_iter ) def minio_remove_object(object_name): - return minio.remove_object( + client = get_minio_client() + return client.remove_object( bucket_name=MINIO['BUCKET_NAME'], object_name=get_refined_object_name(object_name) ) diff --git a/backend/api/services/model_year_report.py b/backend/api/services/model_year_report.py index 5c5beccc2..52d49c923 100644 --- a/backend/api/services/model_year_report.py +++ b/backend/api/services/model_year_report.py @@ -39,8 +39,9 @@ from api.models.model_year_report_make import ModelYearReportMake from api.models.model_year_report_assessment import ModelYearReportAssessment from api.models.model_year_report_assessment_comment import ModelYearReportAssessmentComment +from ..mixins.user_mixin import get_user_data -def get_model_year_report_statuses(report, request_user=None): +def get_model_year_report_statuses(report, request=None): supplier_information_status = 'UNSAVED' consumer_sales_status = 'UNSAVED' compliance_obligation_status = 'UNSAVED' @@ -112,16 +113,11 @@ def get_model_year_report_statuses(report, request_user=None): consumer_sales_status = 'SUBMITTED' compliance_obligation_status = 'SUBMITTED' summary_status = 'SUBMITTED' - - user_profile = UserProfile.objects.filter(username=report.update_user) - - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - - summary_confirmed_by = { - 'create_timestamp': report.update_timestamp, - 'create_user': serializer.data - } + create_user = get_user_data(report.create_user, request) + assessment_confirmed_by = { + 'create_timestamp': report.create_timestamp, ##there are some discrepancies in the + 'create_user': create_user ## create/update timestamps and users so i made a guess here + } if report.validation_status == ModelYearReportStatuses.RECOMMENDED: assessment_status = 'RECOMMENDED' @@ -130,21 +126,17 @@ def get_model_year_report_statuses(report, request_user=None): compliance_obligation_status = 'RECOMMENDED' summary_status = 'RECOMMENDED' - if not request_user.is_government: + if not request.user.is_government: assessment_status = 'SUBMITTED' supplier_information_status = 'SUBMITTED' consumer_sales_status = 'SUBMITTED' compliance_obligation_status = 'SUBMITTED' summary_status = 'SUBMITTED' - - user_profile = UserProfile.objects.filter(username=report.update_user) - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - - assessment_confirmed_by = { - 'create_timestamp': report.update_timestamp, - 'create_user': serializer.data - } + create_user = get_user_data(report.create_user, request) + assessment_confirmed_by = { + 'create_timestamp': report.update_timestamp, + 'create_user': create_user + } if report.validation_status == ModelYearReportStatuses.RETURNED: assessment_status = 'RETURNED' @@ -152,21 +144,17 @@ def get_model_year_report_statuses(report, request_user=None): consumer_sales_status = 'RETURNED' compliance_obligation_status = 'RETURNED' summary_status = 'RETURNED' - if not request_user.is_government: + if not request.user.is_government: assessment_status = 'SUBMITTED' supplier_information_status = 'SUBMITTED' consumer_sales_status = 'SUBMITTED' compliance_obligation_status = 'SUBMITTED' summary_status = 'SUBMITTED' - - user_profile = UserProfile.objects.filter(username=report.update_user) - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - - assessment_confirmed_by = { - 'create_timestamp': report.update_timestamp, - 'create_user': serializer.data - } + create_user = get_user_data(report.create_user, request) + assessment_confirmed_by = { + 'create_timestamp': report.create_timestamp, + 'create_user': create_user + } if report.validation_status == ModelYearReportStatuses.ASSESSED: supplier_information_status = 'ASSESSED' @@ -174,14 +162,11 @@ def get_model_year_report_statuses(report, request_user=None): compliance_obligation_status = 'ASSESSED' summary_status = 'ASSESSED' assessment_status = 'ASSESSED' - user_profile = UserProfile.objects.filter(username=report.update_user) - if user_profile.exists(): - serializer = MemberSerializer(user_profile.first(), read_only=True) - - assessment_confirmed_by = { - 'create_timestamp': report.update_timestamp, - 'create_user': serializer.data - } + create_user = get_user_data(report.create_user, request) + assessment_confirmed_by = { + 'update_timestamp': report.update_timestamp, + 'update_user': create_user + } return { 'supplier_information': { diff --git a/backend/api/services/supplemental_report.py b/backend/api/services/supplemental_report.py index 587bcad74..0df719c6e 100644 --- a/backend/api/services/supplemental_report.py +++ b/backend/api/services/supplemental_report.py @@ -1,5 +1,9 @@ from api.models.supplemental_report import SupplementalReport from api.models.model_year_report_statuses import ModelYearReportStatuses +from api.models.model_year_report import ModelYearReport +from api.models.supplemental_report_credit_activity import ( + SupplementalReportCreditActivity, +) def get_map_of_model_year_report_ids_to_latest_supplemental_ids( @@ -55,3 +59,56 @@ def get_latest_assessed_supplemental(model_year_report): if report_in_question is not None: result = SupplementalReport.objects.get(id=report_in_question.id) return result + + +def get_previous_reassessment_credit_activity(model_year_report_id, category): + result = [] + report = ( + ModelYearReport.objects.filter(id=model_year_report_id) + .select_related("organization", "model_year") + .first() + ) + if report is not None: + previous_model_year = int(report.model_year.name) - 1 + previous_report = ( + ModelYearReport.objects.filter(organization=report.organization) + .filter(model_year__name=previous_model_year) + .filter( + validation_status__in=[ + ModelYearReportStatuses.ASSESSED, + ModelYearReportStatuses.REASSESSED, + ] + ) + .first() + ) + if previous_report is not None: + supplemental_report = ( + SupplementalReport.objects.filter(model_year_report=previous_report) + .filter( + status__in=[ + ModelYearReportStatuses.ASSESSED, + ModelYearReportStatuses.REASSESSED, + ] + ) + .order_by("-create_timestamp") + .first() + ) + if supplemental_report is not None: + result = list( + SupplementalReportCreditActivity.objects.filter( + supplemental_report=supplemental_report + ) + .filter(category=category) + .select_related("model_year") + ) + return result + + +def get_reassessment_credit_activity(supplemental_id, category): + return list( + SupplementalReportCreditActivity.objects.filter( + supplemental_report=supplemental_id + ) + .filter(category=category) + .select_related("model_year") + ) diff --git a/backend/api/tests/test_credit_requests.py b/backend/api/tests/test_credit_requests.py index cde6d57a0..b95a75b86 100644 --- a/backend/api/tests/test_credit_requests.py +++ b/backend/api/tests/test_credit_requests.py @@ -8,23 +8,30 @@ from ..models.vehicle import Vehicle from ..models.vin_statuses import VINStatuses from ..models.sales_submission_statuses import SalesSubmissionStatuses - +from ..models.organization import Organization +from ..models.sales_submission_comment import SalesSubmissionComment class TestSales(BaseTestCase): def setUp(self): super().setUp() - - org1 = self.users['RTAN_BCEID'].organization - - sub = SalesSubmission.objects.create( - organization=org1, + organizations = Organization.objects.filter( + is_government=False, + is_active=True ) - - vehicle = Vehicle.objects.filter(organization=org1).first() + self.user = self.users['RTAN_BCEID'] + filtered_organizations = [org for org in organizations if org != self.user.organization] + self.other_organization = filtered_organizations[0] + self.sub = SalesSubmission.objects.create( + organization=self.user.organization, + ) + self.other_org_sub = SalesSubmission.objects.create( + organization=self.other_organization, + ) + vehicle = Vehicle.objects.filter(organization=self.user.organization).first() if vehicle: ros = RecordOfSale( - submission=sub, + submission=self.sub, vin_validation_status=VINStatuses.UNCHECKED, vin='ABC123', validation_status=RecordOfSaleStatuses.DRAFT, @@ -33,22 +40,90 @@ def setUp(self): ) ros.save() - + self.other_comment = SalesSubmissionComment.objects.create( + create_user="EMHILLIE", + sales_submission=self.sub, + to_govt=True, + comment='test' + ) + self.comment = SalesSubmissionComment.objects.create( + create_user="RTAN", + sales_submission=self.sub, + to_govt=True, + comment='test' + ) + """bceid user can see the list of its own sales""" def test_list_sales_fs(self): response = self.clients['RTAN_BCEID'].get("/api/credit-requests") self.assertEqual(response.status_code, 200) - + """Idir user lists sales""" def test_list_sales_gov(self): response = self.clients['RTAN'].get("/api/credit-requests") self.assertEqual(response.status_code, 200) - + """organization can get the details of their own sale""" + def test_get_submission_details_same_org(self): + response = self.clients['RTAN_BCEID'].get("/api/credit-requests/{}".format(self.sub.id)) + self.assertEqual(response.status_code, 200) + """Idir user cant see draft status submission""" + def test_cant_get_draft_submission_details_idir(self): + response = self.clients['RTAN'].get("/api/credit-requests/{}".format(self.sub.id)) + self.assertEqual(response.status_code, 404) + """BCEID user can submit sales to change the status from draft to submited""" + def test_submit_submission_bceid(self): + response = self.clients['RTAN_BCEID'].patch("/api/credit-requests/{}".format(self.sub.id), + {'validation_status':"SUBMITTED"}, content_type='application/json', + ) + submission = SalesSubmission.objects.get(id=self.sub.id) + self.assertEqual(response.status_code, 200) + self.assertEquals(submission.validation_status, SalesSubmissionStatuses.SUBMITTED) + """organization cannot submit another orgs submission""" + def test_submit_other_org_submission(self): + submission = SalesSubmission.objects.get(id=self.sub.id) + response = self.clients['RTAN_BCEID'].patch("/api/credit-requests/{}".format(self.other_org_sub.id), + {'validation_status':"SUBMITTED"}, content_type='application/json', + ) + submission = SalesSubmission.objects.get(id=self.sub.id) + self.assertEqual(response.status_code, 404) + self.assertEquals(submission.validation_status, SalesSubmissionStatuses.DRAFT) + """bceid user can paginate request list""" + def test_get_paginated_submissions(self): + response = self.clients['RTAN_BCEID'].post("/api/credit-requests/paginated", + {'sorts':[{'id': "id", 'desc': "true"}], 'filters': []}, content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + """a bceid user can download a record of sale template""" + def test_get_record_of_sale_template(self): + response = self.clients['RTAN_BCEID'].get("/api/credit-requests/template") + self.assertEqual(response.status_code, 200) + """user can edit comment that they wrote""" + def test_update_own_comment(self): + response = self.clients['RTAN'].patch("/api/credit-requests/{}/update_comment".format(self.comment.id), + {'comment':"test edit", }, content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + """user cannot edit comment that they didn't write""" + def test_update_others_comment(self): + response = self.clients['RTAN'].patch("/api/credit-requests/{}/update_comment".format(self.other_comment.id), + {'comment':"test edit fail"}, content_type='application/json', + ) + self.assertEqual(response.status_code, 403) + """user can delete their own comment""" + def test_delete_own_comment(self): + response = self.clients['RTAN'].patch("/api/credit-requests/{}/delete_comment".format(self.comment.id), + ) + self.assertEqual(response.status_code, 200) + """user cannot delete another users comment""" + def test_delete_other_comment(self): + response = self.clients['RTAN'].patch("/api/credit-requests/{}/delete_comment".format(self.other_comment.id), + ) + self.assertEqual(response.status_code, 403) + """some submission statuses can not be changed by bceid""" def test_validate_validation_status(self): sub = SalesSubmission.objects.create( organization=self.users['RTAN_BCEID'].organization, submission_sequence=1, validation_status=SalesSubmissionStatuses.NEW ) - request = { 'user': self.users['RTAN_BCEID'] } diff --git a/backend/api/tests/test_organizations.py b/backend/api/tests/test_organizations.py index 01953f6a8..932fd3559 100644 --- a/backend/api/tests/test_organizations.py +++ b/backend/api/tests/test_organizations.py @@ -1,8 +1,231 @@ from .base_test_case import BaseTestCase - +from api.models.organization import Organization +import json +from api.models.organization_ldv_sales import OrganizationLDVSales class TestOrganizations(BaseTestCase): + def setUp(self): + super().setUp() + organizations = Organization.objects.filter( + is_government=False, + is_active=True + ) + self.user = self.users['RTAN_BCEID'] + self.organization = Organization.objects.get(id=self.user.organization_id) + filtered_organizations = [org for org in organizations if org != self.organization] + self.other_organization = filtered_organizations[0] + + """ + BCEID USERS + """ + + """a bceid user can see the list of orgs with names but no balances""" + def test_bceid_get_list_of_orgs(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations") + data = response.json()[0] + response_keys = list(data.keys()) + self.assertEqual(response.status_code, 200) + self.assertNotIn('balance', response_keys) - def test_get_my_organization(self): + """a user can see the details of their own organization """ + def test_bceid_get_my_organization_details(self): response = self.clients['RTAN_BCEID'].get("/api/organizations/mine") + data = response.json() + response_keys = list(data.keys()) + balance = self.organization.balance + self.assertEqual(response.status_code, 200) + self.assertIn('balance', response_keys) + self.assertIn('users', response_keys) + self.assertIn('organizationAddress', response_keys) + self.assertEqual(balance.get('A'), balance['A']) + self.assertEqual(balance.get('B'), balance['B']) + + """a bceid user cannot see balance or sales of a different organization""" + def test_bceid_get_other_organization_details(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}".format(self.other_organization.id)) + data = response.json() + response_keys = list(data.keys()) + self.assertEqual(response.status_code, 404) + self.assertNotIn('balance', response_keys) + self.assertNotIn('users', response_keys) + self.assertNotIn('organizationAddress', response_keys) + + """a bceid user cannot see the users of a different organization""" + def test_bceid_get_other_organization_users(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/users".format(self.other_organization.id)) + data = response.json() + response_keys = list(data.keys()) + self.assertEqual(response.status_code, 404) + self.assertNotIn('users', response_keys) + + """a bceid user cannot see the sales of other orgs""" + def test_bceid_get_sales(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/sales".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user cannot see the transactions of specific orgs""" + def test_bceid_get_supplier_transactions(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/supplier_transactions".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user cannot see the ldv sales of specific orgs""" + def test_bceid_get_ldvsales(self): + response = self.clients['RTAN_BCEID'].put("/api/organizations/{}/ldv_sales".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user cannot see other orgs recent supplier balance""" + def test_bceid_get_recent_supplier_balance(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/recent_supplier_balance".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user cannot see another orgs assessed supplementals""" + def test_bceid_get_assessed_supplementals_map_other_org(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/assessed_supplementals_map".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user can see their own assessed supplementals""" + def test_bceid_get_assessed_supplementals_map(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/assessed_supplementals_map".format(self.organization.id)) + self.assertEqual(response.status_code, 200) + + """a bceid user can see myr ids from their own org""" + def test_bceid_get_most_recent_myr_id(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/most_recent_myr_id".format(self.organization.id)) + self.assertEqual(response.status_code, 200) + + """a bceid user cannot see myr ids from other orgs""" + def test_bceid_get_most_recent_myr_id_other_org(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/most_recent_myr_id".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user cannot see compliance years for another org""" + def test_bceid_get_compliance_years_other_org(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/compliance_years".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user can see compliance years for their own org""" + def test_bceid_get_compliance_years(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/compliance_years".format(self.organization.id)) + self.assertEqual(response.status_code, 200) + + """a bceid user cannot see transactions listed by year for their org""" + def test_bceid_list_by_year(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/list_by_year".format(self.organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user cannot see transactions listed by year for another org""" + def test_bceid_list_by_year_other_org(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/{}/list_by_year".format(self.other_organization.id)) + self.assertEqual(response.status_code, 403) + + """a bceid user can see model years""" + def test_bceid_get_model_years(self): + response = self.clients['RTAN_BCEID'].get("/api/organizations/model_years") + self.assertEqual(response.status_code, 200) + + """ + IDIR USERS + """ + + """an idir user can see the list of orgs with balance, ldv sales, etc""" + def test_idir_get_list_of_orgs(self): + response = self.clients['RTAN'].get("/api/organizations") + data = response.json()[0] + response_keys = list(data.keys()) + self.assertEqual(response.status_code, 200) + self.assertIn('balance', response_keys) + self.assertIn('avgLdvSales', response_keys) + self.assertIn('organizationAddress', response_keys) + self.assertIn('name', response_keys) + self.assertIn('isActive', response_keys) + self.assertIn('supplierClass', response_keys) + self.assertIn('isGovernment', response_keys) + self.assertIn('ldvSales', response_keys) + self.assertIn('hasSubmittedReport', response_keys) + self.assertIn('firstModelYear', response_keys) + self.assertIn('hasReport', response_keys) + + """an idir user can see the balance and sales of an organization""" + def test_idir_get_organization_details(self): + response = self.clients['RTAN'].get("/api/organizations/{}".format(self.other_organization.id)) + data = response.json() + response_keys = list(data.keys()) + self.assertEqual(response.status_code, 200) + self.assertIn('balance', response_keys) + self.assertIn('avgLdvSales', response_keys) + self.assertIn('organizationAddress', response_keys) + self.assertIn('name', response_keys) + self.assertIn('isActive', response_keys) + self.assertIn('supplierClass', response_keys) + self.assertIn('isGovernment', response_keys) + self.assertIn('ldvSales', response_keys) + self.assertIn('hasSubmittedReport', response_keys) + self.assertIn('firstModelYear', response_keys) + self.assertIn('hasReport', response_keys) + + """an idir user can see the users of an organization""" + def test_idir_get_organization_users(self): + response = self.clients['RTAN'].get("/api/organizations/{}/users".format(self.other_organization.id)) + data = response.json() + response_keys = list(data.keys()) + self.assertEqual(response.status_code, 200) + self.assertIn('users', response_keys) + + """an idir user can see the sales of specific orgs""" + def test_idir_get_sales(self): + response = self.clients['RTAN'].get("/api/organizations/{}/sales".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can see the recent supplier balance""" + def test_idir_recent_supplier_balance(self): + response = self.clients['RTAN'].get("/api/organizations/{}/recent_supplier_balance".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can see transactions""" + def test_idir_get_transactions(self): + response = self.clients['RTAN'].get("/api/organizations/{}/supplier_transactions".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can PUT ldv sales""" + def test_idir_put_ldv_sales(self): + response = self.clients['RTAN'].put("/api/organizations/{}/ldv_sales".format(self.organization.id), + {"model_year": '2024', "ldv_sales": '123'}, + content_type='application/json') + self.assertEqual(response.status_code, 200) + + """an idir user can PUT(delete) ldv sales""" + def test_idir_delete_ldv_sales(self): + self.clients['RTAN'].put("/api/organizations/{}/ldv_sales".format(self.organization.id), + {"model_year": '2020', "ldv_sales": '111'}, + content_type='application/json') + ldv = OrganizationLDVSales.objects.first() + response = self.clients['RTAN'].put("/api/organizations/{}/ldv_sales".format(self.organization.id), + {"id": ldv.id}, + content_type='application/json') + self.assertEqual(response.status_code, 200) + + """an idir user can see assessed supplementals map""" + def test_idir_get_assessed_supplementals_map(self): + response = self.clients['RTAN'].get("/api/organizations/{}/assessed_supplementals_map".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can see myr ids for an org""" + def test_idir_get_most_recent_myr_id(self): + response = self.clients['RTAN'].get("/api/organizations/{}/most_recent_myr_id".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can see compliance years for an org""" + def test_idir_get_compliance_years(self): + response = self.clients['RTAN'].get("/api/organizations/{}/compliance_years".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can see transactions listed by year an org""" + def test_idir_list_by_year(self): + response = self.clients['RTAN'].get("/api/organizations/{}/list_by_year".format(self.other_organization.id)) + self.assertEqual(response.status_code, 200) + + """an idir user can see model years""" + def test_idir_get_model_years(self): + response = self.clients['RTAN'].get("/api/organizations/model_years") self.assertEqual(response.status_code, 200) + diff --git a/backend/api/tests/test_users.py b/backend/api/tests/test_users.py new file mode 100644 index 000000000..76e0b1b80 --- /dev/null +++ b/backend/api/tests/test_users.py @@ -0,0 +1,99 @@ +from .base_test_case import BaseTestCase +import random +from ..models.user_profile import UserProfile +from django.urls import reverse +from rest_framework import status +from ..models.organization import Organization +from ..models.role import Role +class TestUsers(BaseTestCase): + def setUp(self): + super().setUp() + organizations = Organization.objects.filter( + is_government=False, + is_active=True + ) + self.user1 = self.users['RTAN_BCEID'] + self.org1 = self.users[self.user1.username].organization + filtered_organizations = [org for org in organizations if org != self.org1] + self.user2 = UserProfile.objects.create( + username="testuser", + organization=self.org1, + first_name="user", + last_name="two", + display_name="test user 2", + keycloak_email="user2@email.com" + ) + other_org = random.choice(filtered_organizations) + self.user3 = UserProfile.objects.create( + username="otherorguser", + organization=other_org, + first_name="user", + last_name="three", + display_name="test user 3", + keycloak_email="user3@email.com" + ) + role_queryset = Role.objects.filter(is_government_role= 'False') + self.roles = list(role_queryset.values_list('id', flat=True)) + + """ assert that a user can get their own details""" + def test_get_current_user(self): + response = self.clients[self.user1.username].get("/api/users/current") + self.assertEqual(response.status_code, 200) + + """user can get details of users in their own org""" + def test_get_details_other_user_in_org(self): + response = self.clients[self.user1.username].get("/api/users/{}".format(self.user2.id)) + self.assertEqual(response.status_code, 200) + + def test_get_details_other_users_other_org(self): + """user cannot get details of users in other orgs""" + response = self.clients[self.user1.username].get("/api/users/{}".format(self.user3.id)) + self.assertEqual(response.status_code, 404) + + """user can update their own profile""" + def test_edit_self(self): + response = self.clients[self.user1.username].put( + "/api/users/{}".format(self.user1.id), + {'first_name':"test change", + 'last_name': self.user1.last_name, + 'title': 'director', + 'username': self.user1.username, + 'keycloak_email': 'test@email.com', + 'roles': self.roles}, + content_type='application/json', + ) + self.assertEquals(response.data['first_name'], 'test change') + self.assertEqual(response.status_code, 200) + + """that user can update other users in their own org""" + def test_edit_user_in_same_org(self): + response = self.clients[self.user1.username].put( + "/api/users/{}".format(self.user2.id), + {'first_name':"test change", + 'last_name': self.user2.last_name, + 'title': 'director', + 'username': self.user2.username, + 'keycloak_email': self.user2.keycloak_email, + 'roles': self.roles}, content_type='application/json',) + + self.assertEquals(response.data['first_name'], 'test change') + self.assertEqual(response.status_code, 200) + + """updating a user from another org should fail""" + def test_edit_user_in_other_org(self): + response = self.clients[self.user1.username].put( + "/api/users/{}".format(self.user3.id), + {'first_name':"test change", + 'last_name': self.user3.last_name, + 'title': 'director', + 'username': self.user3.username, + 'keycloak_email': self.user3.keycloak_email, + 'roles': self.roles + }, content_type='application/json',) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.data['detail'], 'Not found.') + # Ensure that 'first_name' is not in the response data (since it's a 404) + self.assertNotIn('first_name', response.data) + # get the user3 object and make sure the first name hasnt changed + user3_updated = UserProfile.objects.get(id=self.user3.id) + self.assertEquals(user3_updated.first_name, self.user3.first_name) \ No newline at end of file diff --git a/backend/api/urls.py b/backend/api/urls.py index d42a4352f..77cd2688c 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -23,8 +23,8 @@ router = routers.SimpleRouter(trailing_slash=False) router.register(r'organizations', OrganizationViewSet, basename='organization') router.register(r'notifications', NotificationViewSet, basename='notification') -router.register(r'users', UserViewSet) -router.register(r'vehicles', VehicleViewSet) +router.register(r'users', UserViewSet, basename='user') +router.register(r'vehicles', VehicleViewSet, basename='vehicle') router.register(r'roles', RoleViewSet, basename='role') router.register( r'credit-requests', CreditRequestViewset, basename='credit-request' diff --git a/backend/api/viewsets/compliance_ratio.py b/backend/api/viewsets/compliance_ratio.py index 5aea6a79a..90116d749 100644 --- a/backend/api/viewsets/compliance_ratio.py +++ b/backend/api/viewsets/compliance_ratio.py @@ -5,11 +5,13 @@ from api.serializers.compliance_ratio import ComplianceRatioSerializer -class ComplianceRatioViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): +class ComplianceRatioViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): - permission_classes = (permissions.AllowAny,) + permission_classes = [permissions.AllowAny] http_method_names = ['get'] - queryset = ComplianceRatio.objects.all() + + def get_queryset(self): + return ComplianceRatio.objects.all() serializer_classes = { 'default': ComplianceRatioSerializer diff --git a/backend/api/viewsets/credit_agreement.py b/backend/api/viewsets/credit_agreement.py index ecbca464a..531292718 100644 --- a/backend/api/viewsets/credit_agreement.py +++ b/backend/api/viewsets/credit_agreement.py @@ -1,9 +1,8 @@ import uuid -from django.utils.decorators import method_decorator from django.db.models import Q -from rest_framework import mixins, viewsets, permissions +from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response from rest_framework import status @@ -19,27 +18,33 @@ CreditAgreementListSerializer, CreditAgreementSaveSerializer from api.services.minio import minio_put_object from api.services.credit_agreement import adjust_credits -from auditable.views import AuditableMixin +from auditable.views import AuditableCreateMixin, AuditableUpdateMixin from api.models.credit_agreement_statuses import CreditAgreementStatuses from api.services.send_email import notifications_credit_agreement from api.models.model_year_report import ModelYearReport -from api.serializers.model_year_report import ModelYearReportSerializer, ModelYearReportsSerializer +from api.serializers.model_year_report import ModelYearReportsSerializer from api.serializers.credit_agreement_comment import CreditAgreementCommentSerializer from api.services.credit_agreement_comment import get_comment, delete_comment from api.utilities.comment import update_comment_text +from api.permissions.same_organization import SameOrganizationPermissions class CreditAgreementViewSet( - AuditableMixin, viewsets.GenericViewSet, mixins.CreateModelMixin, - mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin + viewsets.GenericViewSet, + AuditableCreateMixin, + AuditableUpdateMixin, + mixins.RetrieveModelMixin, ): - permission_classes = (permissions.AllowAny,) - http_method_names = ['get', 'post', 'put', 'patch'] - queryset = CreditAgreement.objects.all() + permission_classes = [SameOrganizationPermissions] + same_org_permissions_context = { + "default_manager": CreditAgreement.objects, + "default_path_to_org": ("organization",), + "actions_not_to_check": ["retrieve", "partial_update", "minio_url", "update_comment", "delete_comment"] + } + http_method_names = ['get', 'post', 'patch'] serializer_classes = { 'default': CreditAgreementSerializer, 'create': CreditAgreementSaveSerializer, - 'update': CreditAgreementSaveSerializer, 'partial_update': CreditAgreementSaveSerializer, 'list': CreditAgreementListSerializer, } diff --git a/backend/api/viewsets/credit_request.py b/backend/api/viewsets/credit_request.py index 7dfb79685..827003c02 100644 --- a/backend/api/viewsets/credit_request.py +++ b/backend/api/viewsets/credit_request.py @@ -38,23 +38,33 @@ create_errors_spreadsheet, create_details_spreadsheet, ) -from auditable.views import AuditableMixin +from auditable.views import AuditableUpdateMixin import numpy as np from api.paginations import BasicPagination from api.services.filter_utilities import get_search_terms, get_search_q_object from api.services.sales_submission import get_map_of_sales_submission_ids_to_timestamps from api.services.sales_submission import get_warnings_and_maps, get_helping_objects from api.utilities.generic import get_inverse_map +from api.permissions.same_organization import SameOrganizationPermissions class CreditRequestViewset( - AuditableMixin, viewsets.GenericViewSet, - mixins.ListModelMixin, mixins.RetrieveModelMixin, - mixins.UpdateModelMixin + viewsets.GenericViewSet, + AuditableUpdateMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, ): pagination_class = BasicPagination - permission_classes = (CreditRequestPermissions,) - http_method_names = ['get', 'patch', 'post', 'put'] + permission_classes = [SameOrganizationPermissions & CreditRequestPermissions] + same_org_permissions_context = { + "default_manager": SalesSubmission.objects, + "default_path_to_org": ("organization",), + "actions_not_to_check": [ + "retrieve", "partial_update", "download_errors", "content", "unselected", "minio_url", "reasons", + "update_comment", "delete_comment" + ] + } + http_method_names = ['get', 'patch', 'post'] def get_queryset(self): user = self.request.user @@ -78,7 +88,6 @@ def get_queryset(self): 'default': SalesSubmissionListSerializer, 'retrieve': SalesSubmissionSerializer, 'partial_update': SalesSubmissionSaveSerializer, - 'update': SalesSubmissionSaveSerializer, 'content': SalesSubmissionContentSerializer, 'paginated': SalesSubmissionBaseListSerializer } diff --git a/backend/api/viewsets/credit_transaction.py b/backend/api/viewsets/credit_transaction.py index 2386c8e9d..7d79862cb 100644 --- a/backend/api/viewsets/credit_transaction.py +++ b/backend/api/viewsets/credit_transaction.py @@ -1,7 +1,6 @@ from django.db.models import Q -from rest_framework import mixins, viewsets +from rest_framework import viewsets from rest_framework.decorators import action -from rest_framework.permissions import AllowAny from rest_framework.response import Response from api.models.credit_transaction import CreditTransaction @@ -16,15 +15,21 @@ get_compliance_period_bounds, get_timestamp_of_most_recent_reduction ) -from auditable.views import AuditableMixin +from api.permissions.same_organization import SameOrganizationPermissions +from api.models.organization import Organization -class CreditTransactionViewSet( - AuditableMixin, viewsets.GenericViewSet, - mixins.ListModelMixin, mixins.RetrieveModelMixin -): - permission_classes = (AllowAny,) - http_method_names = ['get', 'post', 'put', 'patch'] +class CreditTransactionViewSet(viewsets.GenericViewSet): + permission_classes = [SameOrganizationPermissions] + same_org_permissions_context = { + "custom_pk_actions": { + "calculate_balance": { + "manager": Organization.objects, + "path_to_org": () + } + } + } + http_method_names = ['get'] serializer_classes = { 'default': CreditTransactionSerializer, @@ -58,7 +63,7 @@ def list(self, request): serializer = self.get_serializer(transactions, many=True) return Response(serializer.data) - @action(detail=False) + @action(detail=False, methods=['get']) def recent_balances(self, request): user = self.request.user timestamp = get_timestamp_of_most_recent_reduction(user.organization) @@ -73,9 +78,9 @@ def recent_balances(self, request): serializer = CreditTransactionBalanceSerializer(balances, many=True) return Response(serializer.data) - @action(detail=True) - def calculate_balance(self, request, **kwargs): - org_id = kwargs.pop('pk') + @action(detail=True, methods=['get']) + def calculate_balance(self, request, pk=None): + org_id = pk balances = calculate_insufficient_credits(org_id) serializer = CreditTransactionBalanceSerializer(balances, many=True) return Response(serializer.data) @@ -87,7 +92,7 @@ def compliance_years(self, request): return Response(compliance_years) @action(detail=False, methods=['get']) - def list_by_year(self, request, pk=None): + def list_by_year(self, request): user = self.request.user compliance_year = request.GET.get('year', None) if compliance_year: diff --git a/backend/api/viewsets/credit_transfer.py b/backend/api/viewsets/credit_transfer.py index 878419e73..a4f22f90e 100644 --- a/backend/api/viewsets/credit_transfer.py +++ b/backend/api/viewsets/credit_transfer.py @@ -2,6 +2,7 @@ from rest_framework import mixins, status, viewsets from rest_framework.response import Response from rest_framework.decorators import action +from rest_framework.settings import api_settings from django.db.models import Q from api.models.credit_transfer import CreditTransfer @@ -10,7 +11,7 @@ from api.serializers.credit_transfer import CreditTransferSerializer, \ CreditTransferSaveSerializer, CreditTransferListSerializer, CreditTransferOrganizationBalancesSerializer from api.serializers.credit_transfer_comment import CreditTransferCommentSerializer -from auditable.views import AuditableMixin +from auditable.views import AuditableUpdateMixin from api.services.send_email import notifications_credit_transfers from api.services.credit_transaction import validate_transfer from api.services.credit_transfer_comment import get_comment, delete_comment @@ -20,22 +21,22 @@ class CreditTransferViewset( - AuditableMixin, viewsets.GenericViewSet, - mixins.CreateModelMixin, mixins.ListModelMixin, - mixins.UpdateModelMixin, mixins.RetrieveModelMixin + viewsets.GenericViewSet, + AuditableUpdateMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin ): """ This viewset automatically provides `list`, `create`, `retrieve`, and `update` actions. """ - permission_classes = (CreditTransferPermissions,) - http_method_names = ['get', 'post', 'put', 'patch'] + permission_classes = [CreditTransferPermissions] + http_method_names = ['get', 'post', 'patch'] serializer_classes = { 'default': CreditTransferSerializer, 'create': CreditTransferSaveSerializer, 'partial_update': CreditTransferSaveSerializer, - 'update': CreditTransferSaveSerializer, 'list': CreditTransferListSerializer, } @@ -83,7 +84,11 @@ def create(self, request, *args, **kwargs): credit_transfer = serializer.save() response = credit_transfer - headers = self.get_success_headers(response) + headers = {} + try: + headers = {"Location": str(response[api_settings.URL_FIELD_NAME])} + except (TypeError, KeyError): + pass return Response( response, status=status.HTTP_201_CREATED, headers=headers diff --git a/backend/api/viewsets/dashboard.py b/backend/api/viewsets/dashboard.py index 484247ff4..332ce4c36 100644 --- a/backend/api/viewsets/dashboard.py +++ b/backend/api/viewsets/dashboard.py @@ -1,24 +1,17 @@ from rest_framework import mixins, viewsets -from rest_framework.decorators import action -from rest_framework.response import Response -from api.models.user_profile import UserProfile from api.permissions.user import UserPermissions from api.models.organization import Organization from api.serializers.dashboard import DashboardListSerializer -from auditable.views import AuditableMixin -from api.models.model_year_report_statuses import ModelYearReportStatuses class DashboardViewset( - AuditableMixin, viewsets.GenericViewSet, - mixins.CreateModelMixin, mixins.ListModelMixin, - mixins.UpdateModelMixin, mixins.RetrieveModelMixin + viewsets.GenericViewSet, + mixins.ListModelMixin ): """ - This viewset automatically provides `list`, `create`, `retrieve`, - and `update` actions. + This viewset automatically provides the "list" action """ - permission_classes = (UserPermissions,) + permission_classes = [UserPermissions] http_method_names = ['get'] def get_queryset(self): diff --git a/backend/api/viewsets/icbc_verification.py b/backend/api/viewsets/icbc_verification.py index ffe46d04e..a0d613af1 100644 --- a/backend/api/viewsets/icbc_verification.py +++ b/backend/api/viewsets/icbc_verification.py @@ -1,26 +1,21 @@ import json import os -import urllib.request from django.http import HttpResponse -from rest_framework import mixins, viewsets +from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.permissions import AllowAny from rest_framework.response import Response from api.services.icbc_upload import ingest_icbc_spreadsheet -from api.services.minio import minio_get_object, minio_remove_object +from api.services.minio import get_minio_object, minio_remove_object from api.models.icbc_upload_date import IcbcUploadDate from api.serializers.icbc_upload_date import IcbcUploadDateSerializer -from auditable.views import AuditableMixin -class IcbcVerificationViewSet( - viewsets.ViewSet, AuditableMixin, - viewsets.GenericViewSet, mixins.ListModelMixin -): - permission_classes = (AllowAny,) - http_method_names = ['get', 'post', 'put', 'patch'] +class IcbcVerificationViewSet(viewsets.GenericViewSet): + permission_classes = [AllowAny] + http_method_names = ['get', 'post'] serializer_classes = { 'default': IcbcUploadDateSerializer @@ -40,6 +35,9 @@ def date(self, request): @action(detail=False, methods=['post']) def chunk_upload(self, request): + user = request.user + if not user.is_government: + return Response(status=status.HTTP_403_FORBIDDEN) try: data = request.FILES.get('files') os.rename(data.temporary_file_path(), data.name) @@ -54,8 +52,10 @@ def chunk_upload(self, request): @action(detail=False, methods=['post']) def upload(self, request): user = request.user + if not user.is_government: + return Response(status=status.HTTP_403_FORBIDDEN) + filename = request.data.get('filename') - try: try: # get previous upload file so we can compare @@ -69,28 +69,22 @@ def upload(self, request): print("Last upload date", last_icbc_date.upload_date) - # download previously uploaded file from minio to local directory + # get previous file previous_filename = last_icbc_date.filename - print("Downlading previous file", previous_filename) - last_url = minio_get_object(previous_filename) - urllib.request.urlretrieve(last_url, previous_filename) + print("Downloading previous file", previous_filename) + previous_file = get_minio_object(previous_filename) - # download latest file from minio to local directory - print("Downlading latest file", filename) - url = minio_get_object(filename) - urllib.request.urlretrieve(url, filename) + # get latest file + print("Downloading latest file", filename) + current_file = get_minio_object(filename) print("Starting Ingest") date_current_to = request.data.get('submission_current_date') try: - done = ingest_icbc_spreadsheet(filename, user, date_current_to, previous_filename) + done = ingest_icbc_spreadsheet(current_file, filename, user, date_current_to, previous_file) except: return HttpResponse(status=400, content='Error processing data file. Please contact your administrator for assistance.') - # remove files from local directory - os.remove(filename) - os.remove(previous_filename) - if done[0]: # We remove the previous file from minio but keep the # latest one so we can use it for compare on next upload @@ -99,6 +93,12 @@ def upload(self, request): except Exception as error: return HttpResponse(status=400, content=error) + + finally: + previous_file.close() + previous_file.release_conn() + current_file.close() + current_file.release_conn() return HttpResponse( status=201, diff --git a/backend/api/viewsets/model_year_report.py b/backend/api/viewsets/model_year_report.py index 0f454f79b..fe697e346 100644 --- a/backend/api/viewsets/model_year_report.py +++ b/backend/api/viewsets/model_year_report.py @@ -6,7 +6,7 @@ from rest_framework import mixins, viewsets, status from rest_framework.decorators import action -from auditable.views import AuditableMixin +from auditable.views import AuditableCreateMixin, AuditableUpdateMixin from api.models.model_year import ModelYear from api.models.model_year_report import ModelYearReport from api.models.model_year_report_confirmation import ModelYearReportConfirmation @@ -73,15 +73,14 @@ ) from api.models.organization import Organization from api.services.supplemental_report import get_ordered_list_of_supplemental_reports +from api.permissions.same_organization import SameOrganizationPermissions class ModelYearReportViewset( - AuditableMixin, viewsets.GenericViewSet, + AuditableCreateMixin, + AuditableUpdateMixin, mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, mixins.DestroyModelMixin ): """ @@ -89,14 +88,24 @@ class ModelYearReportViewset( and `update` actions. """ - permission_classes = (ModelYearReportPermissions,) - http_method_names = ["get", "post", "put", "patch", "delete"] + permission_classes = [SameOrganizationPermissions & ModelYearReportPermissions] + same_org_permissions_context = { + "default_manager": ModelYearReport.objects, + "default_path_to_org": ("organization",), + "actions_not_to_check": [ + "retrieve", "partial_update", "destroy", + "noa_history", "supplemental_history", "makes", "submission_confirmation", + "assessment_patch", "comment_patch", "comment_delete", "assessment", + "supplemental", "minio_url", "supplemental_comment_edit", "supplemental_comment_delete", + "supplemental_credit_activity" + ] + } + http_method_names = ["get", "post", "patch", "delete"] serializer_classes = { "default": ModelYearReportSerializer, "create": ModelYearReportSaveSerializer, "list": ModelYearReportListSerializer, - "update": ModelYearReportSaveSerializer, "partial_update": ModelYearReportSaveSerializer, } @@ -159,7 +168,7 @@ def retrieve(self, request, pk=None): model_year_report_id=pk ) - history = ModelYearReportHistorySerializer(history_list, many=True) + history = ModelYearReportHistorySerializer(history_list, many=True, context={'request': request}) confirmations = ( ModelYearReportConfirmation.objects.filter(model_year_report_id=pk) @@ -212,7 +221,7 @@ def retrieve(self, request, pk=None): "create_user": report.create_user, "confirmations": confirmations, "ldv_sales": report.ldv_sales, - "statuses": get_model_year_report_statuses(report, request.user), + "statuses": get_model_year_report_statuses(report, request), "ldv_sales_previous": ldv_sales_previous.data if ldv_sales_previous else [], @@ -417,10 +426,10 @@ def submission_confirmation(self, request, pk=None): return Response({"confirmation": confirmation}) - @action(detail=False, methods=["patch"]) - def submission(self, request): + @action(detail=True, methods=["patch"]) + def submission(self, request, pk=None): validation_status = request.data.get("validation_status") - model_year_report_id = request.data.get("model_year_report_id") + model_year_report_id = pk confirmations = request.data.get("confirmation", None) description = request.data.get("description") remove_submission_confirmation = request.data.get("remove_confirmation", None) @@ -746,7 +755,6 @@ def supplemental(self, request, pk): if not data: data = SupplementalReport() data.model_year_report_id = report.id - serializer = ModelYearReportSupplementalSerializer( data, context={"request": request} ) @@ -1225,7 +1233,7 @@ def assessed_supplementals(self, request, pk): report = get_object_or_404(ModelYearReport, pk=pk) data = report.get_assessed_supplementals() serializer = ModelYearReportSupplementalSerializer( - data, context={"request": request}, many=True + data, many=True, context={"request": request} ) return Response(serializer.data) diff --git a/backend/api/viewsets/model_year_report_compliance_obligation.py b/backend/api/viewsets/model_year_report_compliance_obligation.py index ccf2a0b5e..20e4bcf35 100644 --- a/backend/api/viewsets/model_year_report_compliance_obligation.py +++ b/backend/api/viewsets/model_year_report_compliance_obligation.py @@ -1,5 +1,5 @@ from datetime import date -from rest_framework import mixins, viewsets +from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from django.http import HttpResponse @@ -7,7 +7,6 @@ from django.utils.decorators import method_decorator from django.shortcuts import get_object_or_404 -from auditable.views import AuditableMixin from api.decorators.permission import permission_required from api.models.model_year import ModelYear from api.models.model_year_report import ModelYearReport @@ -38,25 +37,30 @@ from api.services.summary import parse_summary_serializer, \ get_current_year_balance from api.models.organization_deficits import OrganizationDeficits -from api.services.supplemental_report import get_latest_assessed_supplemental +from api.services.supplemental_report import ( + get_latest_assessed_supplemental, + get_previous_reassessment_credit_activity, + get_reassessment_credit_activity +) from api.services.model_year_report_ldv_sales import get_most_recent_ldv_sales - - -class ModelYearReportComplianceObligationViewset( - AuditableMixin, viewsets.GenericViewSet, - mixins.ListModelMixin, mixins.RetrieveModelMixin, - mixins.CreateModelMixin, mixins.UpdateModelMixin -): - """ - This viewset automatically provides `list`, `create`, `retrieve`, - and `update` actions. - """ - permission_classes = (ModelYearReportPermissions,) - http_method_names = ['get', 'post', 'put', 'patch'] - - serializer_classes = { - +from api.permissions.same_organization import SameOrganizationPermissions +from api.models.supplemental_report import SupplementalReport +from api.serializers.model_year_report_supplemental import ModelYearReportSupplementalCreditActivitySerializer + + +class ModelYearReportComplianceObligationViewset(viewsets.GenericViewSet): + permission_classes = [SameOrganizationPermissions & ModelYearReportPermissions] + same_org_permissions_context = { + "default_manager": ModelYearReport.objects, + "default_path_to_org": ("organization",), + "custom_pk_actions": { + "reassessment_credit_activity": { + "manager": SupplementalReport.objects, + "path_to_org": ("model_year_report", "organization") + } + } } + http_method_names = ['get', 'post', 'patch'] def get_queryset(self): request = self.request @@ -65,11 +69,6 @@ def get_queryset(self): ) return queryset - def get_serializer_class(self): - if self.action in list(self.serializer_classes.keys()): - return self.serializer_classes[self.action] - return self.serializer_classes['default'] - def create(self, request, *args, **kwargs): id = request.data.get('report_id') credit_activity = request.data.get('credit_activity') @@ -129,9 +128,9 @@ def create(self, request, *args, **kwargs): return Response(id) - @action(detail=False, methods=['patch']) - def update_obligation(self, request): - id = request.data.get('report_id') + @action(detail=True, methods=['patch']) + def update_obligation(self, request, pk=None): + id = pk credit_activity = request.data.get('credit_activity') records = ModelYearReportComplianceObligation.objects.filter( model_year_report_id=id, @@ -160,17 +159,15 @@ def update_obligation(self, request): status=201, content="Record Updated" ) - @action(detail=False, url_path=r'(?P\d+)') + @action(detail=True, methods=['get']) @method_decorator(permission_required('VIEW_SALES')) - def details(self, request, *args, **kwargs): - summary_param = request.GET.get('summary', None) - summary = True if summary_param == "true" else None + def details(self, request, pk=None): most_recent_ldv_sales_param = request.GET.get("most_recent_ldv_sales", None) most_recent_ldv_sales = True if most_recent_ldv_sales_param == "true" else False organization = request.user.organization - id = kwargs.get('id') + id = pk report = get_object_or_404(ModelYearReport, pk=id) confirmation = ModelYearReportConfirmation.objects.filter( @@ -188,7 +185,7 @@ def details(self, request, *args, **kwargs): if offset_snapshot: offset_serializer = ModelYearReportComplianceObligationOffsetSerializer( offset_snapshot, - context={'request': request, 'kwargs': kwargs}, + context={'request': request}, many=True ) compliance_offset = offset_serializer.data @@ -525,8 +522,7 @@ def details(self, request, *args, **kwargs): serializer = ModelYearReportComplianceObligationSnapshotSerializer( report.get_credit_reductions(), context={ - 'request': request, - 'kwargs': kwargs + 'request': request }, many=True ) @@ -537,10 +533,28 @@ def details(self, request, *args, **kwargs): }) else: serializer = ModelYearReportComplianceObligationSnapshotSerializer( - snapshot, context={'request': request, 'kwargs': kwargs}, many=True + snapshot, context={'request': request}, many=True ) return Response({ 'compliance_obligation': serializer.data, 'compliance_offset': compliance_offset, 'ldv_sales': get_most_recent_ldv_sales(report) if most_recent_ldv_sales else report.ldv_sales }) + + # pk should be MYR id + @action(detail=True, methods=['get']) + def previous_reassessment_credit_activity(self, request, pk=None): + credit_activity = get_previous_reassessment_credit_activity(pk, "ProvisionalBalanceAfterCreditReduction") + serializer = ModelYearReportSupplementalCreditActivitySerializer( + credit_activity, + many=True, + context={"category_transforms": {"ProvisionalBalanceAfterCreditReduction": "PreviousReassessmentEndingBalance"}} + ) + return Response(serializer.data) + + # pk should be a supplemental id + @action(detail=True, methods=['get']) + def reassessment_credit_activity(self, request, pk=None): + credit_activity = get_reassessment_credit_activity(pk, "PreviousReassessmentEndingBalance") + serializer = ModelYearReportSupplementalCreditActivitySerializer(credit_activity, many=True) + return Response(serializer.data) \ No newline at end of file diff --git a/backend/api/viewsets/model_year_report_consumer_sales.py b/backend/api/viewsets/model_year_report_consumer_sales.py index e199fe716..799ecae24 100644 --- a/backend/api/viewsets/model_year_report_consumer_sales.py +++ b/backend/api/viewsets/model_year_report_consumer_sales.py @@ -23,13 +23,16 @@ from api.serializers.vehicle import VehicleSalesSerializer -class ModelYearReportConsumerSalesViewSet(mixins.ListModelMixin, - mixins.CreateModelMixin, - viewsets.GenericViewSet): +class ModelYearReportConsumerSalesViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): - permission_classes = (ModelYearReportPermissions,) - http_method_names = ['get', 'post', 'put', 'patch'] - queryset = ModelYearReport.objects.all() + permission_classes = [ModelYearReportPermissions] + http_method_names = ['get', 'post'] + + def get_queryset(self): + user = self.request.user + if user.is_government: + return ModelYearReport.objects.all() + return ModelYearReport.objects.filter(organization=user.organization) serializer_classes = { 'default': ModelYearReportSerializer, @@ -146,7 +149,7 @@ def retrieve(self, request, pk): create_user__in=users ) - history = ModelYearReportHistorySerializer(history_list, many=True) + history = ModelYearReportHistorySerializer(history_list, many=True, context={'request': request}) validation_status = report.validation_status.value if not request.user.is_government and \ diff --git a/backend/api/viewsets/notification.py b/backend/api/viewsets/notification.py index ea0794833..1dec23d92 100644 --- a/backend/api/viewsets/notification.py +++ b/backend/api/viewsets/notification.py @@ -1,37 +1,31 @@ import logging -from django.db.models import Q from django.http import HttpResponseBadRequest -from rest_framework import filters, mixins, status, permissions, viewsets +from rest_framework import mixins, permissions, viewsets from rest_framework.response import Response from rest_framework.decorators import action -from auditable.views import AuditableMixin from api.models.notification import Notification from api.models.notification_subscription import NotificationSubscription from api.serializers.notification import NotificationSerializer -from api.serializers.notification_subscription import NotificationSubscriptionSerializer LOGGER = logging.getLogger(__name__) class NotificationViewSet( - AuditableMixin, - mixins.CreateModelMixin, + viewsets.GenericViewSet, mixins.ListModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet ): """ This viewset automatically provides `list` """ - permission_classes = (permissions.AllowAny,) - http_method_names = ['get', 'post', 'put'] - queryset = Notification.objects.all().order_by('name') + permission_classes = [permissions.AllowAny] + http_method_names = ['get', 'post'] + + def get_queryset(self): + return Notification.objects.all().order_by('name') serializer_classes = { 'default': NotificationSerializer, - 'create': NotificationSubscriptionSerializer, - 'update': NotificationSubscriptionSerializer } def get_serializer_class(self): diff --git a/backend/api/viewsets/organization.py b/backend/api/viewsets/organization.py index 106963ddb..1f0b59d57 100644 --- a/backend/api/viewsets/organization.py +++ b/backend/api/viewsets/organization.py @@ -2,7 +2,7 @@ from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response - +from rest_framework import status from api.decorators.permission import permission_required from api.models.organization import Organization from api.models.organization_ldv_sales import OrganizationLDVSales @@ -17,7 +17,8 @@ from api.serializers.organization_ldv_sales import \ OrganizationLDVSalesSerializer from api.permissions.organization import OrganizationPermissions -from auditable.views import AuditableMixin +from api.permissions.same_organization import SameOrganizationPermissions +from auditable.views import AuditableCreateMixin, AuditableUpdateMixin from api.services.supplemental_report import get_map_of_model_year_report_ids_to_latest_supplemental_ids from api.services.credit_transaction import ( aggregate_credit_balance_details, @@ -37,21 +38,35 @@ class OrganizationViewSet( - AuditableMixin, viewsets.GenericViewSet, - mixins.CreateModelMixin, mixins.ListModelMixin, - mixins.UpdateModelMixin, mixins.RetrieveModelMixin + viewsets.GenericViewSet, + AuditableCreateMixin, + AuditableUpdateMixin, + mixins.RetrieveModelMixin ): """ This viewset automatically provides `list`, `create`, `retrieve`, and `update` actions. """ - permission_classes = (OrganizationPermissions,) + permission_classes = [SameOrganizationPermissions & OrganizationPermissions] http_method_names = ['get', 'post', 'put', 'patch'] - + same_org_permissions_context = { + "default_manager": Organization.objects, + "default_path_to_org": (), + "actions_not_to_check": [ + "retrieve", + "update", + "partial_update", + "users", + "sales", + "recent_supplier_balance", + "supplier_transactions", + "ldv_sales", + "list_by_year", + ] + } serializer_classes = { 'default': OrganizationSerializer, 'mine': OrganizationWithMembersSerializer, - 'update': OrganizationSaveSerializer, 'create': OrganizationSaveSerializer, 'partial_update': OrganizationSaveSerializer, 'sales': SalesSubmissionListSerializer, @@ -88,7 +103,7 @@ def list(self, request): serializer = OrganizationNameSerializer if request.user.is_government: serializer = OrganizationSerializer - + return Response(serializer(organizations, many=True).data) @action(detail=False) @@ -124,7 +139,7 @@ def sales(self, request, pk=None): Get the sales submissions of a specific organization """ if not request.user.is_government: - return Response(None) + return Response({'detail': 'You do not have permission to perform this action.'}, status=status.HTTP_403_FORBIDDEN) sales = SalesSubmission.objects.filter( organization_id=pk @@ -144,7 +159,7 @@ def sales(self, request, pk=None): @method_decorator(permission_required('VIEW_SALES')) def recent_supplier_balance(self, request, pk=None): if not request.user.is_government: - return Response(None) + return Response({'detail': 'You do not have permission to perform this action.'}, status=status.HTTP_403_FORBIDDEN) timestamp = get_timestamp_of_most_recent_reduction(pk) q_obj = None @@ -165,19 +180,19 @@ def supplier_transactions(self, request, pk=None): Get the list of transactions of a specific organization """ if not request.user.is_government: - return Response(None) + return Response({'detail': 'You do not have permission to perform this action.'}, status=status.HTTP_403_FORBIDDEN) transactions = aggregate_transactions_by_submission(pk) serializer = CreditTransactionListSerializer(transactions, many=True) return Response(serializer.data) - @action(detail=True, methods=['patch', 'put']) + @action(detail=True, methods=['put']) @method_decorator(permission_required('VIEW_SALES')) def ldv_sales(self, request, pk=None): delete_id = request.data.get('id', None) if not request.user.is_government: - return Response(None) + return Response({'detail': 'You do not have permission to perform this action.'}, status=status.HTTP_403_FORBIDDEN) organization = self.get_object() if delete_id: @@ -233,7 +248,7 @@ def compliance_years(self, request, pk=None): @method_decorator(permission_required('VIEW_SALES')) def list_by_year(self, request, pk=None): if not request.user.is_government: - return Response([]) + return Response({'detail': 'You do not have permission to perform this action.'}, status=status.HTTP_403_FORBIDDEN) compliance_year = request.GET.get('year', None) if compliance_year: compliance_period_bounds = get_compliance_period_bounds(compliance_year) @@ -244,8 +259,8 @@ def list_by_year(self, request, pk=None): return Response(serializer.data) return Response([]) - @action(detail=True, methods=['get']) - def model_years(self, request, pk=None): + @action(detail=False, methods=['get']) + def model_years(self, request): model_years = get_model_years() serializer = ModelYearSerializer(model_years, many=True) return Response(serializer.data) diff --git a/backend/api/viewsets/role.py b/backend/api/viewsets/role.py index 1582955da..1940bc98e 100644 --- a/backend/api/viewsets/role.py +++ b/backend/api/viewsets/role.py @@ -5,11 +5,13 @@ from api.serializers.role import RoleSerializer -class RoleViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): +class RoleViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): - permission_classes = (RolePermissions,) + permission_classes = [RolePermissions] http_method_names = ['get'] - queryset = Role.objects.all() + + def get_queryset(self): + return Role.objects.all() serializer_classes = { 'default': RoleSerializer diff --git a/backend/api/viewsets/sales_forecast.py b/backend/api/viewsets/sales_forecast.py index 41a984ccf..cbcaa5ae3 100644 --- a/backend/api/viewsets/sales_forecast.py +++ b/backend/api/viewsets/sales_forecast.py @@ -4,6 +4,8 @@ from rest_framework import status from api.paginations import BasicPagination from api.permissions.sales_forecast import SalesForecastPermissions +from api.permissions.same_organization import SameOrganizationPermissions +from api.models.model_year_report import ModelYearReport from api.services.sales_forecast import ( update_or_create, delete_records, @@ -19,7 +21,12 @@ class SalesForecastViewset(viewsets.GenericViewSet): - permission_classes = [SalesForecastPermissions] + permission_classes = [SameOrganizationPermissions & SalesForecastPermissions] + same_org_permissions_context = { + "default_manager": ModelYearReport.objects, + "default_path_to_org": ("organization",), + } + http_method_names = ['get', 'post'] pagination_class = BasicPagination # pk should be a myr_id diff --git a/backend/api/viewsets/signing_authority_assertion.py b/backend/api/viewsets/signing_authority_assertion.py index 5524bf422..0c02730b9 100644 --- a/backend/api/viewsets/signing_authority_assertion.py +++ b/backend/api/viewsets/signing_authority_assertion.py @@ -3,21 +3,19 @@ from django.db.models import Q from rest_framework import filters, mixins, permissions, viewsets -from auditable.views import AuditableMixin - from api.models.signing_authority_assertion import SigningAuthorityAssertion from api.serializers.signing_authority_assertion import \ SigningAuthorityAssertionSerializer class SigningAuthorityAssertionViewSet( - AuditableMixin, mixins.ListModelMixin, - viewsets.GenericViewSet + viewsets.GenericViewSet, + mixins.ListModelMixin, ): """ This viewset automatically provides `list` """ - permission_classes = (permissions.AllowAny,) + permission_classes = [permissions.AllowAny] http_method_names = ['get'] queryset = SigningAuthorityAssertion.objects.all() filter_backends = (filters.OrderingFilter,) diff --git a/backend/api/viewsets/upload.py b/backend/api/viewsets/upload.py index e1e0b0491..bdb2742e4 100644 --- a/backend/api/viewsets/upload.py +++ b/backend/api/viewsets/upload.py @@ -9,7 +9,7 @@ class UploadViewSet(ViewSet): - permission_classes = (UploadPermissions,) + permission_classes = [UploadPermissions] http_method_names = ['get'] @action(detail=False, methods=['get']) diff --git a/backend/api/viewsets/user.py b/backend/api/viewsets/user.py index 1b737a079..40055124a 100644 --- a/backend/api/viewsets/user.py +++ b/backend/api/viewsets/user.py @@ -1,26 +1,33 @@ from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response - +from rest_framework import status from api.models.user_profile import UserProfile from api.permissions.user import UserPermissions from api.serializers.user import UserSerializer, UserSaveSerializer from api.services.bceid_email_spreadsheet import create_bceid_emails_sheet -from auditable.views import AuditableMixin +from auditable.views import AuditableCreateMixin, AuditableUpdateMixin class UserViewSet( - AuditableMixin, viewsets.GenericViewSet, - mixins.CreateModelMixin, mixins.ListModelMixin, - mixins.UpdateModelMixin, mixins.RetrieveModelMixin + viewsets.GenericViewSet, + AuditableCreateMixin, + AuditableUpdateMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin ): """ This viewset automatically provides `list`, `create`, `retrieve`, and `update` actions. """ - permission_classes = (UserPermissions,) - http_method_names = ['get', 'post', 'put', 'patch'] - queryset = UserProfile.objects.all() + permission_classes = [UserPermissions] + http_method_names = ['get', 'post', 'put'] + + def get_queryset(self): + user = self.request.user + if user.is_government: + return UserProfile.objects.all() + return UserProfile.objects.filter(organization=user.organization) serializer_classes = { 'default': UserSerializer, @@ -46,7 +53,9 @@ def current(self, request): @action(detail=False) def download_active(self, request): - - active_bceid_users_excel = create_bceid_emails_sheet() - - return active_bceid_users_excel + user = self.request.user + if user.is_government: + active_bceid_users_excel = create_bceid_emails_sheet() + return active_bceid_users_excel + else: + return Response({'detail': 'You do not have permission to perform this action.'}, status=status.HTTP_403_FORBIDDEN) diff --git a/backend/api/viewsets/vehicle.py b/backend/api/viewsets/vehicle.py index 633c3590f..7ffc8df28 100644 --- a/backend/api/viewsets/vehicle.py +++ b/backend/api/viewsets/vehicle.py @@ -18,17 +18,18 @@ VehicleStatusChangeSerializer, VehicleIsActiveChangeSerializer, \ VehicleListSerializer from api.services.minio import minio_put_object -from auditable.views import AuditableMixin +from auditable.views import AuditableCreateMixin, AuditableUpdateMixin from api.models.vehicle import VehicleDefinitionStatuses from api.services.send_email import notifications_zev_model class VehicleViewSet( - AuditableMixin, viewsets.GenericViewSet, mixins.CreateModelMixin, - mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin + viewsets.GenericViewSet, + AuditableCreateMixin, + AuditableUpdateMixin, + mixins.RetrieveModelMixin, ): - permission_classes = (VehiclePermissions,) - http_method_names = ['get', 'post', 'put', 'patch'] - queryset = Vehicle.objects.all() + permission_classes = [VehiclePermissions] + http_method_names = ['get', 'post', 'patch'] serializer_classes = { 'default': VehicleSerializer, @@ -115,7 +116,7 @@ def is_active_change(self, request, pk=None): change active / inactive status for vehicle """ serializer = self.get_serializer( - self.queryset.get(id=pk), + self.get_object(), data=request.data ) @@ -132,7 +133,7 @@ def state_change(self, request, pk=None): Update the state of a vehicle """ serializer = self.get_serializer( - self.queryset.get(id=pk), + self.get_object(), data=request.data ) diff --git a/backend/auditable/views.py b/backend/auditable/views.py index 4f972e1e9..cb189ebe1 100644 --- a/backend/auditable/views.py +++ b/backend/auditable/views.py @@ -1,15 +1,12 @@ -from django.shortcuts import render - from rest_framework.response import Response from rest_framework import status - -from api.models.user_profile import UserProfile +from rest_framework.settings import api_settings -class AuditableMixin(object,): +class AuditableCreateMixin: def serialize_object(self, request, data): user = request.user - data.update({'create_user': user.username, 'update_user': user.username}) + data.update({"create_user": user.username, "update_user": user.username}) serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) @@ -28,23 +25,26 @@ def create(self, request, *args, **kwargs): return Response(response, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): - instance = serializer.save() + serializer.save() + + def get_success_headers(self, data): + try: + return {"Location": str(data[api_settings.URL_FIELD_NAME])} + except (TypeError, KeyError): + return {} - def update(self, request, *args, **kwargs): - if request.method == 'PATCH': - partial = kwargs.pop('partial', True) - else: - partial = kwargs.pop('partial', False) +class AuditableUpdateMixin: + def update(self, request, *args, **kwargs): + partial = kwargs.pop("partial", False) instance = self.get_object() user = request.user - request.data.update({'update_user': user.id}) - serializer = self.get_serializer(instance, data=request.data, - partial=partial) + request.data.update({"update_user": user.id}) + serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) - if getattr(instance, '_prefetched_objects_cache', None): + if getattr(instance, "_prefetched_objects_cache", None): # If 'prefetch_related' has been applied to a queryset, we need to # forcibly invalidate the prefetch cache on the instance. instance._prefetched_objects_cache = {} @@ -53,3 +53,7 @@ def update(self, request, *args, **kwargs): def perform_update(self, serializer): serializer.save() + + def partial_update(self, request, *args, **kwargs): + kwargs["partial"] = True + return self.update(request, *args, **kwargs) diff --git a/backend/zeva/settings.py b/backend/zeva/settings.py index 29914709a..4282f1e73 100644 --- a/backend/zeva/settings.py +++ b/backend/zeva/settings.py @@ -84,7 +84,7 @@ 'DEFAULT_AUTHENTICATION_CLASSES': ( 'api.keycloak_authentication.UserAuthentication',), 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated',), + 'api.permissions.allow_none.AllowNone',), # 'EXCEPTION_HANDLER': 'core.exceptions.exception_handler', 'DEFAULT_RENDERER_CLASSES': ( 'djangorestframework_camel_case.render.CamelCaseJSONRenderer', diff --git a/frontend/.s2i/bin/assemble b/frontend/.s2i/bin/assemble deleted file mode 100755 index 19faefa24..000000000 --- a/frontend/.s2i/bin/assemble +++ /dev/null @@ -1,130 +0,0 @@ -sh-5.1$ cat assemble -#!/bin/bash - -# Prevent running assemble in builders different than official STI image. -# The official nodejs:8-onbuild already run npm install and use different -# application folder. -[ -d "/usr/src/app" ] && exit 0 - -set -e - -# FIXME: Linking of global modules is disabled for now as it causes npm failures -# under RHEL7 -# Global modules good to have -# npmgl=$(grep "^\s*[^#\s]" ../etc/npm_global_module_list | sort -u) -# Available global modules; only match top-level npm packages -#global_modules=$(npm ls -g 2> /dev/null | perl -ne 'print "$1\n" if /^\S+\s(\S+)\@[\d\.-]+/' | sort -u) -# List all modules in common -#module_list=$(/usr/bin/comm -12 <(echo "${global_modules}") | tr '\n' ' ') -# Link the modules -#npm link $module_list - -safeLogging () { - if [[ $1 =~ http[s]?://.*@.*$ ]]; then - echo $1 | sed 's/^.*@/redacted@/' - else - echo $1 - fi -} - -shopt -s dotglob -if [ -d /tmp/artifacts ] && [ "$(ls /tmp/artifacts/ 2>/dev/null)" ]; then - echo "---> Restoring previous build artifacts ..." - mv -T --verbose /tmp/artifacts/node_modules "${HOME}/node_modules" -fi - -echo "---> Installing application source ..." -mv /tmp/src/* ./ - -# Fix source directory permissions -fix-permissions ./ - -if [ ! -z $HTTP_PROXY ]; then - echo "---> Setting npm http proxy to" $(safeLogging $HTTP_PROXY) - npm config set proxy $HTTP_PROXY -fi - -if [ ! -z $http_proxy ]; then - echo "---> Setting npm http proxy to" $(safeLogging $http_proxy) - npm config set proxy $http_proxy -fi - -if [ ! -z $HTTPS_PROXY ]; then - echo "---> Setting npm https proxy to" $(safeLogging $HTTPS_PROXY) - npm config set https-proxy $HTTPS_PROXY -fi - -if [ ! -z $https_proxy ]; then - echo "---> Setting npm https proxy to" $(safeLogging $https_proxy) - npm config set https-proxy $https_proxy -fi - -# Change the npm registry mirror if provided -if [ -n "$NPM_MIRROR" ]; then - npm config set registry $NPM_MIRROR -fi - -# Set the DEV_MODE to false by default. -if [ -z "$DEV_MODE" ]; then - export DEV_MODE=false -fi - -# If NODE_ENV is not set by the user, then NODE_ENV is determined by whether -# the container is run in development mode. -if [ -z "$NODE_ENV" ]; then - if [ "$DEV_MODE" == true ]; then - export NODE_ENV=development - else - export NODE_ENV=production - fi -fi - -if [ "$NODE_ENV" != "production" ]; then - - echo "---> Building your Node application from source" - npm install - -else - - echo "---> Have to set DEV_MODE and NODE_ENV to empty otherwise the deployment can not be started" - echo "---> It'll have error like can not resolve source-map-loader..." - export DEV_MODE="" - export NODE_ENV="" - - if [[ -z "${ARTIFACTORY_USER}" ]]; then - echo "---> Installing all dependencies from external repo" - else - echo "---> Installing all dependencies from Artifactory" - npm config set registry https://artifacts.developer.gov.bc.ca/artifactory/api/npm/npm-remote/ - curl -u $ARTIFACTORY_USER:$ARTIFACTORY_PASSWORD https://artifacts.developer.gov.bc.ca/artifactory/api/npm/auth >> ~/.npmrc - fi - - echo "---> Installing all dependencies" - NODE_ENV=development npm install - - #do not fail when there is no build script - echo "---> Building in production mode" - npm run build --if-present - - echo "---> Pruning the development dependencies" - npm prune - - NPM_TMP=$(npm config get tmp) - if ! mountpoint $NPM_TMP; then - echo "---> Cleaning the $NPM_TMP/npm-*" - rm -rf $NPM_TMP/npm-* - fi - - # Clear the npm's cache and tmp directories only if they are not a docker volumes - NPM_CACHE=$(npm config get cache) - if ! mountpoint $NPM_CACHE; then - echo "---> Cleaning the npm cache $NPM_CACHE" - #As of npm@5 even the 'npm cache clean --force' does not fully remove the cache directory - # instead of $NPM_CACHE* use $NPM_CACHE/*. - # We do not want to delete .npmrc file. - rm -rf "${NPM_CACHE:?}/" - fi -fi - -# Fix source directory permissions -fix-permissions ./ \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 823d3beef..c148eba2a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "zeva-frontend", - "version": "1.63.0", + "version": "1.64.0", "private": true, "dependencies": { "@babel/eslint-parser": "^7.19.1", diff --git a/frontend/src/app/router.js b/frontend/src/app/router.js index 5df1e79f8..e77bc1271 100644 --- a/frontend/src/app/router.js +++ b/frontend/src/app/router.js @@ -352,10 +352,13 @@ class Router extends Component { ( + user.isGovernment + && ( + ) )} /> { const result = { reconciledAssessmentData: assessmentData, @@ -151,6 +152,21 @@ const reconcileSupplementaries = ( } } + if (complianceData?.complianceObligation && previousReassessmentCreditActivities) { + const creditActivites = complianceData.complianceObligation + for (const previousReassessmentCreditActivity of previousReassessmentCreditActivities) { + const category = previousReassessmentCreditActivity.category + const modelYear = previousReassessmentCreditActivity.modelYear.name + if (category === 'PreviousReassessmentEndingBalance') { + const creditActivityInQuestion = creditActivites.find((activity) => activity.category === 'creditBalanceStart' && activity.modelYear.name === modelYear) + if (creditActivityInQuestion) { + creditActivityInQuestion.creditAValue = previousReassessmentCreditActivity.creditAValue + creditActivityInQuestion.creditBValue = previousReassessmentCreditActivity.creditBValue + } + } + } + } + return result } diff --git a/frontend/src/compliance/AssessmentContainer.js b/frontend/src/compliance/AssessmentContainer.js index 12c5fa48c..cc9bc3e72 100644 --- a/frontend/src/compliance/AssessmentContainer.js +++ b/frontend/src/compliance/AssessmentContainer.js @@ -566,15 +566,13 @@ const AssessmentContainer = (props) => { } const ObligationData = { - reportId: id, creditActivity: reportDetailsArray } - axios.patch(ROUTES_COMPLIANCE.OBLIGATION_SAVE, ObligationData) + axios.patch(ROUTES_COMPLIANCE.OBLIGATION_SAVE.replace(/:id/g, id), ObligationData) } const data = { - modelYearReportId: id, validation_status: status, modelYear: reportYear } @@ -590,7 +588,7 @@ const AssessmentContainer = (props) => { } } - axios.patch(ROUTES_COMPLIANCE.REPORT_SUBMISSION, data).then(() => { + axios.patch(ROUTES_COMPLIANCE.REPORT_SUBMISSION.replace(':id', id), data).then(() => { if (status === 'DRAFT' && analystAction) { history.push(ROUTES_COMPLIANCE.REPORTS) } else { diff --git a/frontend/src/compliance/ComplianceReportSummaryContainer.js b/frontend/src/compliance/ComplianceReportSummaryContainer.js index e3eb5be52..eb8bb6f48 100644 --- a/frontend/src/compliance/ComplianceReportSummaryContainer.js +++ b/frontend/src/compliance/ComplianceReportSummaryContainer.js @@ -43,12 +43,11 @@ const ComplianceReportSummaryContainer = (props) => { } const handleSubmit = (status) => { const data = { - modelYearReportId: id, validation_status: status, confirmation: checkboxes } - axios.patch(ROUTES_COMPLIANCE.REPORT_SUBMISSION, data).then(() => { + axios.patch(ROUTES_COMPLIANCE.REPORT_SUBMISSION.replace(':id', id), data).then(() => { history.push(ROUTES_COMPLIANCE.REPORTS) history.replace(ROUTES_COMPLIANCE.REPORT_SUMMARY.replace(':id', id)) }) diff --git a/frontend/src/creditagreements/components/CreditAgreementsAlert.js b/frontend/src/creditagreements/components/CreditAgreementsAlert.js index a3aa2f003..1c774efd0 100644 --- a/frontend/src/creditagreements/components/CreditAgreementsAlert.js +++ b/frontend/src/creditagreements/components/CreditAgreementsAlert.js @@ -30,13 +30,13 @@ const CreditAgreementsAlert = (props) => { switch (status) { case 'DRAFT': title = 'Draft' - message = `saved, ${date} by ${typeof updateUser === 'string' ? updateUser : updateUser.firstName + " " + updateUser.lastName}.` + message = `saved, ${date} by ${updateUser.displayName}.` classname = 'alert-warning' break case 'RECOMMENDED': title = 'Recommended' - message = `recommended for issuance, ${date} by ${typeof updateUser === 'string' ? updateUser : updateUser.firstName + " " + updateUser.lastName}.` + message = `recommended for issuance, ${date} by ${updateUser.displayName}.` classname = 'alert-primary' break @@ -72,7 +72,7 @@ CreditAgreementsAlert.defaultProps = { CreditAgreementsAlert.propTypes = { date: PropTypes.string.isRequired, - user: PropTypes.string.isRequired, + updateUser: PropTypes.shape().isRequired, status: PropTypes.string.isRequired, transactionType: PropTypes.string.isRequired, id: PropTypes.string.isRequired, diff --git a/frontend/src/credits/components/CreditTransfersDetailsPage.js b/frontend/src/credits/components/CreditTransfersDetailsPage.js index 5a749d11e..2616ff1ce 100644 --- a/frontend/src/credits/components/CreditTransfersDetailsPage.js +++ b/frontend/src/credits/components/CreditTransfersDetailsPage.js @@ -336,8 +336,9 @@ const CreditTransfersDetailsPage = (props) => { {latestSubmit && (
Signed and submitted by {latestSubmit.createUser.displayName}{' '} - of  - {latestSubmit.createUser.organization.name}  + {latestSubmit.createUser.organization && + <>of {latestSubmit.createUser.organization.name}  + } {moment(latestSubmit.createTimestamp) .tz('America/Vancouver') .format('YYYY-MM-DD hh:mm:ss z')} diff --git a/frontend/src/organizations/VehicleSupplierEditContainer.js b/frontend/src/organizations/VehicleSupplierEditContainer.js index 64b7c8c1b..c916ec115 100644 --- a/frontend/src/organizations/VehicleSupplierEditContainer.js +++ b/frontend/src/organizations/VehicleSupplierEditContainer.js @@ -68,11 +68,7 @@ const VehicleSupplierEditContainer = (props) => { } const getModelYears = () => { - let orgId = id - if (newSupplier) { - orgId = -1 - } - axios.get(ROUTES_ORGANIZATIONS.MODEL_YEARS.replace(/:id/gi, orgId)).then((response) => { + axios.get(ROUTES_ORGANIZATIONS.MODEL_YEARS).then((response) => { const modelYearObjects = response.data const modelYearNames = [] modelYearObjects.forEach((modelYearObject) => { diff --git a/frontend/src/supplementary/SupplementaryContainer.js b/frontend/src/supplementary/SupplementaryContainer.js index 20d07de6b..2f0efa7f1 100644 --- a/frontend/src/supplementary/SupplementaryContainer.js +++ b/frontend/src/supplementary/SupplementaryContainer.js @@ -41,6 +41,7 @@ const SupplementaryContainer = (props) => { ]) const location = useLocation() const [reassessmentReductions, setReassessmentReductions] = useState({}) + const [previousReassessmentCreditActivities, setPreviousReassessmentCreditActivities] = useState([]) const query = qs.parse(location.search, { ignoreQueryPrefix: true }) @@ -460,6 +461,17 @@ const SupplementaryContainer = (props) => { } creditActivity.push(creditActivityAddition) } + if (!supplementaryId) { + for (const each of previousReassessmentCreditActivities) { + const category = each.category + if (category === 'PreviousReassessmentEndingBalance') { + const creditActivityAddition = { ... each } + creditActivityAddition.modelYear = each.modelYear.name + delete creditActivityAddition.id + creditActivity.push(creditActivityAddition) + } + } + } setNewData({ ...newData, creditActivity }) if (status) { @@ -575,28 +587,38 @@ const SupplementaryContainer = (props) => { const refreshDetails = () => { setLoading(true) + const promises = [ + axios.get( + `${ROUTES_SUPPLEMENTARY.DETAILS.replace(':id', id)}?supplemental_id=${ + supplementaryId || '' + }` + ), + axios.get( + `${ROUTES_SUPPLEMENTARY.ASSESSED_SUPPLEMENTALS.replace(':id', id)}` + ), + axios.get( + `${ROUTES_COMPLIANCE.REPORT_COMPLIANCE_DETAILS_BY_ID.replace(':id', id)}?most_recent_ldv_sales=true&use_from_gov_snapshot=True` + ), + axios.get(ROUTES_COMPLIANCE.RATIOS), + axios.get( + `${ROUTES_SUPPLEMENTARY.ASSESSMENT.replace( + ':id', + id + )}?supplemental_id=${supplementaryId || ''}` + ) + ] + if (supplementaryId) { + promises.push( + axios.get(ROUTES_COMPLIANCE.REASSESSMENT_CREDIT_ACTIVITY.replace(':supp_id', supplementaryId)) + ) + } else { + promises.push( + axios.get(ROUTES_COMPLIANCE.PREVIOUS_REASSESSMENT_CREDIT_ACTIVITY.replace(':id', id)) + ) + } axios - .all([ - axios.get( - `${ROUTES_SUPPLEMENTARY.DETAILS.replace(':id', id)}?supplemental_id=${ - supplementaryId || '' - }` - ), - axios.get( - `${ROUTES_SUPPLEMENTARY.ASSESSED_SUPPLEMENTALS.replace(':id', id)}` - ), - axios.get( - `${ROUTES_COMPLIANCE.REPORT_COMPLIANCE_DETAILS_BY_ID.replace(':id', id)}?most_recent_ldv_sales=true&use_from_gov_snapshot=True` - ), - axios.get(ROUTES_COMPLIANCE.RATIOS), - axios.get( - `${ROUTES_SUPPLEMENTARY.ASSESSMENT.replace( - ':id', - id - )}?supplemental_id=${supplementaryId || ''}` - ) - ]) + .all(promises) .then( axios.spread( ( @@ -604,7 +626,8 @@ const SupplementaryContainer = (props) => { assessedSupplementals, complianceResponse, ratioResponse, - assessmentResponse + assessmentResponse, + previousReassessmentCreditActivityResponse ) => { if (response.data) { let assessedSupplementalsData = assessedSupplementals.data @@ -635,6 +658,7 @@ const SupplementaryContainer = (props) => { } } } + const previousReassessmentCreditActivityResponseData = previousReassessmentCreditActivityResponse.data const { reconciledAssessmentData, reconciledLdvSales, @@ -642,7 +666,8 @@ const SupplementaryContainer = (props) => { } = reconcileSupplementaries( response.data.assessmentData, assessedSupplementalsData, - complianceResponse.data + complianceResponse.data, + previousReassessmentCreditActivityResponseData ) if (reconciledAssessmentData) { response.data.assessmentData = reconciledAssessmentData @@ -803,6 +828,7 @@ const SupplementaryContainer = (props) => { if (reconciledComplianceObligation) { setObligationDetails(reconciledComplianceObligation) + setPreviousReassessmentCreditActivities(previousReassessmentCreditActivityResponseData) } if (reconciledLdvSales) { diff --git a/frontend/src/users/components/UserDetailsForm.js b/frontend/src/users/components/UserDetailsForm.js index 98471bca5..6cd6cdde5 100644 --- a/frontend/src/users/components/UserDetailsForm.js +++ b/frontend/src/users/components/UserDetailsForm.js @@ -100,7 +100,7 @@ const UserDetailsForm = (props) => {
{ name="firstName" /> { name="lastName" /> { name="title" /> { name="username" /> { name="keycloakEmail" /> { /> {accountType === 'BCeID' && ( 7V1bl5vIEf41Osk+jA7d3B89Y2eTEzvxWduJvS85jMRIxIzQIsYzzq9PIy6CrtII3bpaoBdbtIBBdev6vqpuRubd48uvabCcf0imYTzixvRlZL4dce5xT/ybD/wsBizTLwZmaTQththm4FP0v7AcNMrRp2garlonZkkSZ9GyPThJFotwkrXGgjRNntunPSRx+68ug1kIBj5NghiO/juaZvPyZ9nGZvyvYTSbV3+ZGeU3j0F1cjmwmgfT5LkxZL4bmXdpkmTFp8eXuzDOZVfJpbjuL1u+rR8sDRdZlwsmYXq79Cz372n2n48u//D9/fsfN8wsHy77Wf3icCoEUB4maTZPZskiiN9tRm/T5GkxDfPbGuJoc877JFmKQSYG/xtm2c9Sm8FTloihefYYl9+GL1H2tfH5W36rsV0evX0p77w++FkdLLL059fmQeOq/HBz2fqouq74ffmP2iq3cmiVPKWT8DVhWaUBBukszF470a31K/wiTB5D8UTiwjSMgyz60X6SoLTQWX3eRoniQ6nHfXRaPuaPIH4q/9Qdg2qOY+FDuTqf51EWfloG69/+LLy4raxgtSwc6yF6yZVeSvRHmGbhy+syhRKoLmCeXVxThgXTKr3keeNkbjk0b/hXNXZ6odlXR+juCG5XR/BIHcGh1OlGj98a3+zS6UaN31pa1EenjcnrMJ2Wl35MIvGMm5Bg+kYrJNhM8vXiycrLJNOon+MIa3Fh2OSahU3T1i1selBoSEpBKjSb6SY0/zrXdI5LVfq8Oy6ZlHNN9ZhNR3ijmSPI0cPyiB2h+g0NoT0tV1kaBo/ifkJxYbAKb9jYdMcGkKUQRNYWmrgy+R7eJXGSipFFssgF/RDFsTQUxNFsIQ4nQnChGL/NxRoJzPem/OIxmk7jbVpqO+Ip9GKYUoByoF6YhSjGPJtiOFBMkkazaCHu9hAG2VMa3nx/ChY9Vgo32kqp05GmUjiiFDltOZ1STBhibjULMbahW4iBYDiYTnMmKBIS48aXL1/YOHvJemzJteVWloyEF0dpdLFf10kcCdGtwr6rhbfVYiEBRq1aSOHypaWlXeGyeSxcPk6nCKjVjwtsO4LjUs8Z3tUROjuC2RWfmVvMQJEjkGJuY+wwt6HXG2N8HkIwv8nHMI2E0PI57uTK9roq+1gwvo0klOoGniUFgTOThCYC8/UjCTWLpyaE+RqShLoJDTGr6yT0arjpEpccyknIRBC8biShzJZ7CFpU6wgQwQ+TJJQClA/1opYkNCGMHxpJaDptZ/ENRClKSULTgSFGN5LQcXQLMRAlD40krPOdypJtqBOlbJQJK8tDJAml2dhFatdq1eIDtcDospi+yVsvN2KdBqv5Wi6srRZMYLsTQiiuhjRsRBrV2LHo0/Db2nBkMReJKkCf8E4O33GnM+PYyoxaanTi3GtWy3zKbujT+eMp71G9fUgW2c1qjRxE0mowvnwR/631ZtwHk++ztTZvJoVH5aeks/s/c8vLz+LiOQ1u+ZvPtvHL5t7i0yz/P3jI3S7X1L3I6qpHEj+xeKriJGhwwmTeB/dhLE1enZ05DcXvCu7X98t/0TKX+1oT9u3IfvuaM5eNx+XFo7rdt2m1r3jSVte/McaG4/otK+HHmXF1SvLwsArPY1YI0teNbjakmY4ZSPrxWhQ5eUy1OJAaGzN7bIxhbO3P1Ab1YEI9YICGn00PNM3wFX1SHyinT6yuje0WKYdvIY3tSPZBW8zSL7pAmC6ii9NrsoS59kiz2ILgct048FoildQYAgLV2i4E5h+C1TpF7K3tGoYcQSpjJrNdCMVFBOk33coc2RccqAVfqRYg8h6Pxz1Wgcx40wfxKs51JD8mcbBaRZMu6V598Epd/2vj87fGZ7wqNzpheljOXbvTQwvXqCJ6xpTnr9pB9+Zn5MVIm7lREUFjI7BZYb/K6IDyLx1+sXlXAyUt/1aP2cwBLahm2uYROeiS54A26RL1S/OErki+sEUyT0CQPLIAm9QTLK6dJ0Akrx+7KufNnFxqEHkPgV0FeuDUybNLEsc1iMldm2YL/yaLychSbgTe0LKr+kUXiMr7z65K/Tv0scWBwFw/dtXTzXYdWK3tO7vqA1yP0HpqTReCwt6TqxYgapAAopRcdWBnds/JVRnd0MdwiAlhBD+IXN2d7R2C9kenyxCr6Wt3hrhlPy1VBKtMDtXL+/cnWOXZ0FLcAeeQbvh2YbSS07UCUFgyFYRxkCK7dgSr3CBCnwbSAPML9YSuYN4hLTU4CJjXjmDVDsw7tIvEj/aEXVXkE/qB2zVlcUi3AK0e89Wkkna9pYyFTKTLQakXuNfS8x5+wDv6gUuaGVWPqa8fODK2oPcDyAmwMfOGxcxgm6UqZWbcC9jNHBTbLGT3CLW2izZ9D67oaSHWq5TUcmn2VNNgXuy6P5q7pWdQ0bx4AZt+gyUl9NEFbQgfWNGTPrbA0rN+RU85q8M2H1VquxXvPOSip+URFz09WHnufdGzbh+sXQFZ11O/PkqNGiAqHFjVkzyIexBkwhDevyUl1dy1Oz3c8iotqoqna0iWcHjF05HnuDNXPD2kFHbl9bYJyz7SQNXgFw9pCdet4sm1Y0c82heDXZgndEbyWza0UeQJCJLXreJpyukHPRoi3er80jyha9Li0c4JCC+ABDza2j/TzRN8yAsUnNYYVhv6A4i4rRur5UNqIJunYb7l43OQP/pjKLyvx2wN5MwsZFdZtTpBMOl1ltgmrK57kXuk+ZKP7EWONAISv01Mu1kCVqMHFp1Ah0D9Hiuy6HTt5N4jOnXt5PZIF6P6SCc3AlVoo5N2dVkfYuDedyw5IB5Rb5fmw+o4tNxdlD7yHpaub4zSP7b4xKQ+qIfKbxboTOqD2dCSzerMpL6PgH3t2+Owd5coDZR1mXXg/XEOkk8rjZXMoGk2p0/EmMG7RktSnFg/p9YtchpGGLR7fGA9chrEF6R9XPsmOQ8h/BRbL6wrD65LzjWIgYSYr7EYMrA2OY8Rb7zMDAirB9Ymp0EcPwWovrw+uXr+2pkllnmaPp1yvmwMh3fKeTI+PzOoZsaFLwRXi2SqWv7hNqoIylQPelHdcvRQhqHd+r1vjdAPzDBkzruGpa3S6lqILydZurCElOK1a12UG7boMSq79rPv4w1dG9pLc6TzBqSlXbf2RVBB0sAbkIq59oUg7C3TisWGlswHVwiqwRpdbkPTia5DZO7aVl76OF1kRmrNSEaqVyWIPsRwiDqHVwmiDzAcotg780+ama9MfXEDkZti8+VAboMrBfnUu8QzjpaT+10KsuUyhI9QYWpLQRyWkwdWCtIgkEOACKN4PzeKryex3bki12vjhBwit29ycDlIjCguB/HrevF9bLTrgvESUJDhGY4sGde+HKQBntm2+9uwykEaTITXKvUeYamaNDpMnVtMQVFYqh60VQ7SDajKm/drAFRNmlbyS3UH3tUdCnukcwfIP9w5urmDvIu/Du4AGYPPz8motVC5x/O16cnztUc9X5tIL7puS+4dWwZb5JVN8xR4/xLfsVIbTIcYzXG1qkL7rhz+Dkb78mvXuaEa7Zto1z0sT/YnVjpA5kisVEu02ki1Rrd2Bllq1Mv/mQ1TJTa2rF4XCWQlUO9fxWxkedkwGhnsztOVTYuwbSQRQ3IKyuDC9Qsu2CtlLLvPwYXzNobQILggXX6aLWc1WVto2O51ii0X8vyPZQeDcZ8Gi8m8vyYsJ+bUb35gNqweiKgfBqvwRoSTdVeUE+eyvxfqcWbFp37riEvb3FnIitd6RbYiLcFWv173OcglZvJAX9HrQ1vvanfdQ6pM4qgoD3kFgH3oeyHkHaQsxW+FYM61bLKHfTq8s33SNmtXD6pxc4Nl6JYpOjTY/VJ9oTPeL4yRzhcQvK/ZAkvL184XkNKUvjzWNFjN13+XndrIj05IVBk5Qg2csrqISPgoi3dkaErOcLkw564gqZa45+Qa0Q4FIS+a/fjbiJuMW17hWQ3i4OZmxJ3gMZfTmjYQJ5Q0j/lmxBFeYfXHk5Bg/uCL6WhbR8RG5Gy305xaI/IG+Njb2GvxN1Ui10xPpxJkw9m34Y/jxHYCSQmTGLdnUPRdAQiJcjZRcWST2Y9pMiWXlS35uQaSgtXez+EqI5eU5WtoVUOtL/LOO+aWCT9VKsaxLXM128JCjgHkeIMjuyxCke1gPNtmXttsw0y/tax0BwNaY/Dd2++fDX90J0S37ROthhDlEiFqyRGwKyFqSoSoKbeSnZkQ5afYarKHhtid+aQ1RFPqYK1XKuxtiNWLPKqpXzEzzw2Yal8NcR/akdgQpSUyB79lxDIkQ5Qt+tyGiOybSWSILaM6xCpPaIh75KO0hujI/QVyIOtsiEwyRMX78nJkJ9KrIe5hiCXQvnxDrEAwmSFChHedmveYmokNUV6ud/jUzCRDVA1WkL1jr4a4V0TcsrpVkSHKYOXQdVMWlwzRV22IsMb++7t/5YWXX//2Wfz7IZmGMTBNPQpp5ZMfVX+Qtoeoc+ZmDydDDOl8RDGyke4/l+FiNY8ecsP5R/AYrnKprPqrFSbtQoSx9wxz7wO0Ig7TJMmabiV+1bywe/Pd/wE= \ No newline at end of file diff --git a/openshift/templates/knp/knp-diagram.drawio b/openshift/templates/knp/knp-diagram.drawio deleted file mode 100644 index 9b032b9b6..000000000 --- a/openshift/templates/knp/knp-diagram.drawio +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/openshift/templates/knp/knp-quick-start.yaml b/openshift/templates/knp/knp-quick-start.yaml deleted file mode 100644 index e7a97a6b3..000000000 --- a/openshift/templates/knp/knp-quick-start.yaml +++ /dev/null @@ -1,60 +0,0 @@ ---- -apiVersion: template.openshift.io/v1 -kind: Template -labels: - template: zeva-network-policy -metadata: - name: zeva-network-policy -parameters: - - name: ENVIRONMENT - displayName: null - description: such as dev, test or prod - required: true - - name: NAMESPACE_PREFIX - displayName: null - description: the namespace prefix - required: true -objects: - - kind: NetworkPolicy - apiVersion: networking.k8s.io/v1 - metadata: - name: deny-by-default - spec: - # The default posture for a security first namespace is to - # deny all traffic. If not added this rule will be added - # by Platform Services during environment cut-over. - podSelector: {} - ingress: [] - - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: allow-from-openshift-ingress - spec: - # This policy allows any pod with a route & service combination - # to accept traffic from the OpenShift router pods. This is - # required for things outside of OpenShift (like the Internet) - # to reach your pods. - ingress: - - from: - - namespaceSelector: - matchLabels: - network.openshift.io/policy-group: ingress - podSelector: {} - policyTypes: - - Ingress - - apiVersion: networking.k8s.io/v1 - kind: NetworkPolicy - metadata: - name: allow-all-internal - spec: - # Allow all pods within the current namespace to communicate - # to one another. - ingress: - - from: - - namespaceSelector: - matchLabels: - environment: ${ENVIRONMENT} - name: ${NAMESPACE_PREFIX} - podSelector: {} - policyTypes: - - Ingress \ No newline at end of file