diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 43b67a443..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: 2.1 - -orbs: - redhat-openshift: circleci/redhat-openshift@0.2.0 - -jobs: - build-in-Openshift: - executor: redhat-openshift/default - steps: - - checkout - - redhat-openshift/login-and-update-kubeconfig: - insecure-skip-tls-verify: true - openshift-platform-version: 3.x - server-address: $OC_SERVER_ADDRESS - token: $OC_TOKEN - - run: - name: test oc connection to path finder - command: oc projects - - run: - name: build frontend - command: | - oc start-build envoy --wait=true -n tbiwaq-tools - oc start-build frontend --wait=true -n tbiwaq-tools - oc start-build python-backend --wait=true -n tbiwaq-tools -workflows: - version: 2 - build-deploy: - jobs: - - build-in-Openshift \ No newline at end of file diff --git a/.github/workflows/build-new-python-image.yaml b/.github/workflows/build-new-python-image.yaml new file mode 100644 index 000000000..d2220811f --- /dev/null +++ b/.github/workflows/build-new-python-image.yaml @@ -0,0 +1,37 @@ +## For each release, the value of workflow name, branches, PR_NUMBER and RELEASE_NAME need to be adjusted accordingly +## Also change the .pipelin/lib/config.js version number +name: new-python-image + +on: + push: + branches: [ new-python39-image-1.42.0 ] + workflow_dispatch: + workflow_call: + +env: + ## The pull request number of the Tracking pull request to merge the release branch to main + PR_NUMBER: 1102 + RELEASE_NAME: new-python39-image-1.42.0 + +jobs: + + ## This is the CI job + build: + + name: Build ZEVA on Openshift + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + + ## it will checkout to /home/runner/work/zeva/zeva + - name: Check out repository + uses: actions/checkout@v2 + + # open it when zeva updated the python packages + - name: Run django tests + uses: kuanfandevops/django-test-action@zeva-django-test + with: + settings-dir-path: "backend/zeva" + requirements-file: "backend/requirements.txt" + managepy-dir: backend diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml index e27eee2f7..65ed875ce 100644 --- a/.github/workflows/build-release.yaml +++ b/.github/workflows/build-release.yaml @@ -1,17 +1,17 @@ ## For each release, the value of workflow name, branches, PR_NUMBER and RELEASE_NAME need to be adjusted accordingly ## Also change the .pipelin/lib/config.js version number -name: CI/CD ZEVA release-1.41.0 +name: ZEVA v1.42.0 on: push: - branches: [ release-1.41.0 ] + branches: [ release-1.42.0 ] workflow_dispatch: workflow_call: env: ## The pull request number of the Tracking pull request to merge the release branch to main - PR_NUMBER: 1072 - RELEASE_NAME: release-1.41.0 + PR_NUMBER: 1091 + RELEASE_NAME: release-1.42.0 jobs: @@ -28,6 +28,14 @@ jobs: - name: Check out repository uses: actions/checkout@v2 + # open it when zeva updated the python packages + #- name: Run django tests + # uses: kuanfandevops/django-test-action@zeva-django-test + # with: + # settings-dir-path: "backend/zeva" + # requirements-file: "backend/requirements.txt" + # managepy-dir: backend + ## Log in to Openshift with a token of service account - name: Log in to Openshift uses: redhat-actions/oc-login@v1 @@ -44,12 +52,39 @@ jobs: npm install npm run build -- --pr=${{ env.PR_NUMBER }} --env=build + deploy-on-dev: + + name: Deploy ZEVA on Dev Environment + runs-on: ubuntu-latest + timeout-minutes: 60 + needs: build + + steps: + + - name: Check out repository + uses: actions/checkout@v2 + + - name: Log in to Openshift + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + openshift_token: ${{ secrets.OPENSHIFT_TOKEN }} + insecure_skip_tls_verify: true + namespace: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-tools + + #- name: Run BCDK deployment on ZEVA Dev environment + # run: | + # cd .pipeline + # echo "Deploying ZEVA ${{ env.RELEASE_NAME }} on Dev" + # npm install + # npm run deploy -- --pr=${{ env.PR_NUMBER }} --env=dev + deploy-on-test: name: Deploy ZEVA on Test Environment runs-on: ubuntu-latest timeout-minutes: 60 - needs: build + needs: deploy-on-dev steps: @@ -112,4 +147,4 @@ jobs: cd .pipeline echo "Deploying ZEVA ${{ env.RELEASE_NAME }} on Prod" npm install - npm run deploy -- --pr=${{ env.PR_NUMBER }} --env=prod \ No newline at end of file + npm run deploy -- --pr=${{ env.PR_NUMBER }} --env=prod diff --git a/.jenkins/.pipeline/lib/config.js b/.jenkins/.pipeline/lib/config.js index 15dfcbb36..4bee555ce 100644 --- a/.jenkins/.pipeline/lib/config.js +++ b/.jenkins/.pipeline/lib/config.js @@ -1,7 +1,7 @@ 'use strict'; const options= require('@bcgov/pipeline-cli').Util.parseArguments() const changeId = options.pr //aka pull-request -const version = '1.0.0' +const version = '1.42.0' const name = 'jenkins' const ocpName = 'apps.silver.devops' diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index 9340aa0b5..b0febdc47 100644 --- a/.pipeline/lib/config.js +++ b/.pipeline/lib/config.js @@ -1,7 +1,7 @@ 'use strict'; const options= require('@bcgov/pipeline-cli').Util.parseArguments() const changeId = options.pr //aka pull-request -const version = '1.41.0' +const version = '1.42.0' const name = 'zeva' const ocpName = 'apps.silver.devops' diff --git a/.pipeline/lib/deploy.js b/.pipeline/lib/deploy.js index 6ec09f087..29e4368cf 100755 --- a/.pipeline/lib/deploy.js +++ b/.pipeline/lib/deploy.js @@ -148,6 +148,7 @@ module.exports = settings => { })) //deploy schemaspy + /* if(phase === 'dev') { objects = objects.concat(oc.processDeploymentTemplate(`${templatesLocalBaseUrl}/templates/schemaspy/schemaspy-dc.yaml`, { 'param': { @@ -161,7 +162,7 @@ module.exports = settings => { 'OCP_NAME': phases[phase].ocpName } })) - } + }*/ /** //deploy rabbitmq, use docker image directly diff --git a/backend/.s2i/bin/assemble b/backend/.s2i/bin/assemble old mode 100644 new mode 100755 index 0767891df..1b547cbc4 --- a/backend/.s2i/bin/assemble +++ b/backend/.s2i/bin/assemble @@ -8,58 +8,104 @@ function should_collectstatic() { is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] } -# Install pipenv to the separate virtualenv to isolate it +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_pipenv() { - echo "---> Installing pipenv packaging tool ..." - VENV_DIR=$HOME/.local/venvs/pipenv - virtualenv $VENV_DIR - $VENV_DIR/bin/pip --isolated install -U pipenv +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/pipenv $HOME/.local/bin/pipenv + 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/* ./ +mv /tmp/src/* "$HOME" -if [[ ! -z "$UPGRADE_PIP_TO_LATEST" || ! -z "$ENABLE_PIPENV" ]]; then - echo "---> Upgrading pip to latest version ..." - pip install -U pip setuptools wheel +# 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 - install_pipenv + 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 + # 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 - echo "---> Installing dependencies ..." - pip install -i https://$ARTIFACTORY_USER:$ARTIFACTORY_PASSWORD@artifacts.developer.gov.bc.ca/artifactory/api/pypi/pypi-remote/simple --upgrade pip - pip install -i https://$ARTIFACTORY_USER:$ARTIFACTORY_PASSWORD@artifacts.developer.gov.bc.ca/artifactory/api/pypi/pypi-remote/simple -r requirements.txt -elif [[ -f setup.py ]]; then - echo "---> Installing application ..." - python setup.py develop + 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 + 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=${APP_HOME:-.} - # Look for 'manage.py' in the directory specified by APP_HOME, or the current directory - manage_file=$APP_HOME/manage.py + 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." @@ -75,20 +121,8 @@ if should_collectstatic; then fi python $manage_file collectstatic --noinput - ) fi -echo "---> current folder is " -pwd - -# Run unit tests in build stage with the code below -# echo "--> running Django unit tests" - -# python $manage_file test - # set permissions for any installed artifacts -fix-permissions /opt/app-root -echo "---> current folder2 is " -pwd -ls -lrt +fix-permissions /opt/app-root -P \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index a937e1ddd..1343bdc97 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6 +FROM python:3.9 ENV PYTHONUNBUFFERED=1 diff --git a/backend/api/models/sales_submission_content.py b/backend/api/models/sales_submission_content.py index 00d8ec528..398eae4da 100644 --- a/backend/api/models/sales_submission_content.py +++ b/backend/api/models/sales_submission_content.py @@ -84,7 +84,10 @@ def vehicle(self): @property def icbc_verification(self): - q = 'select * from icbc_registration_data where vin=\'{}\' limit 1'.format(self.xls_vin) + q = 'select * from icbc_registration_data join icbc_upload_date on \ + icbc_upload_date.id = icbc_upload_date_id where \ + vin=\'{}\' and upload_date < \'{}\' limit 1'.format( + self.xls_vin, self.update_timestamp) registration = IcbcRegistrationData.objects.raw(q) if registration: return registration[0] @@ -113,6 +116,8 @@ def is_already_awarded(self): submission_id=self.submission_id ).filter( vin=self.xls_vin + ).exclude( + create_timestamp__gte=self.update_timestamp ).first() if has_been_awarded: diff --git a/backend/api/serializers/sales_submission_comment.py b/backend/api/serializers/sales_submission_comment.py index cd53f9529..727740659 100644 --- a/backend/api/serializers/sales_submission_comment.py +++ b/backend/api/serializers/sales_submission_comment.py @@ -22,10 +22,15 @@ def get_create_user(self, obj): serializer = MemberSerializer(user, read_only=True) return serializer.data + def update(self, instance, validated_data): + instance.comment = validated_data.get("comment") + instance.save() + return instance + class Meta: model = SalesSubmissionComment fields = ( - 'id', 'comment', 'create_timestamp', 'create_user','to_govt' + 'id', 'comment', 'create_timestamp', 'create_user','to_govt', 'update_timestamp' ) read_only_fields = ( 'id', diff --git a/backend/api/serializers/vehicle.py b/backend/api/serializers/vehicle.py index c2b4aa774..b258f1af0 100644 --- a/backend/api/serializers/vehicle.py +++ b/backend/api/serializers/vehicle.py @@ -5,6 +5,7 @@ from api.models.model_year import ModelYear from api.models.credit_class import CreditClass +from api.models.organization import Organization from api.models.vehicle import Vehicle from api.models.vehicle_attachment import VehicleAttachment from api.models.vehicle_change_history import VehicleChangeHistory @@ -150,6 +151,45 @@ class Meta: fields = ('create_timestamp', 'create_user', 'validation_status') +class VehicleListSerializer( + ModelSerializer, EnumSupportSerializerMixin +): + organization = SerializerMethodField() + validation_status = EnumField(VehicleDefinitionStatuses, read_only=True) + credit_value = SerializerMethodField() + credit_class = SerializerMethodField() + model_year = SerializerMethodField() + vehicle_zev_type = SerializerMethodField() + + def get_organization(self, obj): + organization = Organization.objects.get(id=obj.organization_id) + name = organization.name + short_name = organization.short_name + return {'name': name, 'short_name': short_name} + + def get_credit_value(self, instance): + return instance.get_credit_value() + + def get_credit_class(self, instance): + return instance.get_credit_class() + + def get_model_year(self, obj): + model_year = ModelYear.objects.get(id=obj.model_year_id) + return model_year.name + + def get_vehicle_zev_type(self, obj): + zev_type = ZevType.objects.filter(id=obj.vehicle_zev_type_id).first() + return zev_type.vehicle_zev_code + + class Meta: + model = Vehicle + fields = ('id', 'organization', 'validation_status', + 'credit_value', 'credit_class', + 'model_year', 'model_name', 'make', + 'range', 'vehicle_zev_type', 'is_active' + ) + + class VehicleSerializer( ModelSerializer, EnumSupportSerializerMixin ): diff --git a/backend/api/services/send_email.py b/backend/api/services/send_email.py index a2a3976b7..e7dfd192a 100644 --- a/backend/api/services/send_email.py +++ b/backend/api/services/send_email.py @@ -271,8 +271,10 @@ def notifications_zev_model(request: object, validation_status: str): def subscribed_users(notifications: list, request: object, request_type: str, email_type: str): user_email = None try: - subscribed_users = NotificationSubscription.objects.values_list('user_profile_id', flat=True).filter(notification__id__in=notifications) - + subscribed_users = NotificationSubscription.objects.values_list('user_profile_id', flat=True).filter( + notification__id__in=notifications).filter( + user_profile__is_active=True + ) if subscribed_users: govt_org = Organization.objects.filter(is_government=True).first() if request_type == 'credit_transfer': diff --git a/backend/api/tests/test_credit_requests.py b/backend/api/tests/test_credit_requests.py index dd73154ac..cde6d57a0 100644 --- a/backend/api/tests/test_credit_requests.py +++ b/backend/api/tests/test_credit_requests.py @@ -7,6 +7,7 @@ from ..models.sales_submission import SalesSubmission from ..models.vehicle import Vehicle from ..models.vin_statuses import VINStatuses +from ..models.sales_submission_statuses import SalesSubmissionStatuses class TestSales(BaseTestCase): @@ -45,7 +46,7 @@ def test_validate_validation_status(self): sub = SalesSubmission.objects.create( organization=self.users['RTAN_BCEID'].organization, submission_sequence=1, - validation_status='DRAFT' + validation_status=SalesSubmissionStatuses.NEW ) request = { @@ -55,17 +56,15 @@ def test_validate_validation_status(self): # try changing from status NEW to VALIDATED, this should fail # ie it should throw a Validation Error self.assertRaises( - ValidationError, sub.validate_validation_status( - 'VALIDATED', request - ) + ValidationError, sub.validate_validation_status, SalesSubmissionStatuses.VALIDATED, request ) - sub.validation_status = 'RECOMMEND_APPROVAL' + sub.validation_status = SalesSubmissionStatuses.RECOMMEND_APPROVAL sub.save() # try changing from status RECOMMEND_APPROVAL to DELETED, this should # fail # ie it should throw a Validation Error self.assertRaises( - ValidationError, sub.validate_validation_status('DELETED', request) + ValidationError, sub.validate_validation_status, SalesSubmissionStatuses.DELETED, request ) diff --git a/backend/api/tests/test_model_year_report_status.py b/backend/api/tests/test_model_year_report_status.py deleted file mode 100644 index f79249796..000000000 --- a/backend/api/tests/test_model_year_report_status.py +++ /dev/null @@ -1,44 +0,0 @@ -# from django.utils.datetime_safe import datetime -# from rest_framework.serializers import ValidationError - -# from .base_test_case import BaseTestCase -# from ..models.model_year_report import ModelYearReport -# from ..models.supplemental_report import SupplementalReport -# from ..models.model_year_report_statuses import ModelYearReportStatuses -# from ..models.organization import Organization -# from ..models.model_year import ModelYear - - -# class TestModelYearReports(BaseTestCase): -# def setUp(self): -# super().setUp() - -# org1 = self.users['EMHILLIE_BCEID'].organization -# gov = self.users['RTAN'].organization - -# model_year_report = ModelYearReport.objects.create( -# organization=org1, -# create_user='EMHILLIE_BCEID', -# validation_status=ModelYearReportStatuses.ASSESSED, -# organization_name=Organization.objects.get('BMW Canada Inc.'), -# supplier_class='M', -# model_year=ModelYear.objects.get('2021'), -# credit_reduction_selection='A' -# ) -# supplementary_report = SupplementalReport.objects.create( -# create_user='EMHILLIE_BCEID', -# validation_status=ModelYearReportStatuses.DRAFT, -# ) -# reassessment_report = SupplementalReport.objects.create( -# create_user='RTAN', -# validation_status=ModelYearReportStatuses.DRAFT, -# ) - -# def test_status(self): -# response = self.clients['EMHILLIE_BCEID'].get("/api/compliance/reports") -# self.assertEqual(response.status_code, 200) -# result = response.data -# print('(((((((((())))))))))') -# print(result) -# print('(((((((((())))))))))') -# # self.assertEqual(len(result), 1) diff --git a/backend/api/tests/test_model_year_reports.py b/backend/api/tests/test_model_year_reports.py new file mode 100644 index 000000000..95d5b030a --- /dev/null +++ b/backend/api/tests/test_model_year_reports.py @@ -0,0 +1,121 @@ +from email import header +import json +from django.utils.datetime_safe import datetime +from rest_framework.serializers import ValidationError + +from .base_test_case import BaseTestCase +from ..models.model_year_report import ModelYearReport +from ..models.supplemental_report import SupplementalReport +from ..models.model_year_report_statuses import ModelYearReportStatuses +from ..models.model_year_report_assessment import ModelYearReportAssessment +from ..models.model_year_report_assessment_descriptions import ModelYearReportAssessmentDescriptions +from ..models.model_year_report_ldv_sales import ModelYearReportLDVSales +from ..models.organization import Organization +from ..models.model_year import ModelYear + + +class TestModelYearReports(BaseTestCase): + def setUp(self): + super().setUp() + + org1 = self.users['EMHILLIE_BCEID'].organization + gov = self.users['RTAN'].organization + + model_year_report = ModelYearReport.objects.create( + organization=org1, + create_user='EMHILLIE_BCEID', + validation_status=ModelYearReportStatuses.ASSESSED, + organization_name=Organization.objects.get(name='BMW Canada Inc.'), + supplier_class='M', + model_year=ModelYear.objects.get(effective_date='2021-01-01'), + credit_reduction_selection='A' + ) + supplementary_report = SupplementalReport.objects.create( + model_year_report=model_year_report, + create_user='EMHILLIE_BCEID', + status=ModelYearReportStatuses.DRAFT, + ) + model_year_report_assessment_description = ModelYearReportAssessmentDescriptions.objects.create( + description='test', + display_order=1 + ) + model_year_report_assessment = ModelYearReportAssessment.objects.create( + model_year_report=model_year_report, + model_year_report_assessment_description=model_year_report_assessment_description, + penalty=20.00 + ) + reassessment_report = SupplementalReport.objects.create( + model_year_report=model_year_report, + supplemental_id=supplementary_report.id, + create_user='RTAN', + status=ModelYearReportStatuses.DRAFT, + ) + + def test_status(self): + response = self.clients['EMHILLIE_BCEID'].get("/api/compliance/reports") + self.assertEqual(response.status_code, 200) + result = response.data + self.assertEqual(len(result), 1) + + + def test_assessment_patch_response(self): + makes = ["TESLATRUCK", "TESLA", "TEST"] + sales = {"2020":25} + data = json.dumps({"makes":makes, "sales":sales}) + response = self.clients['RTAN'].patch("/api/compliance/reports/1/assessment_patch", data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + response = self.clients['RTAN'].patch("/api/compliance/reports/999/assessment_patch", data=data, content_type='application/json') + self.assertEqual(response.status_code, 404) + + + def test_assessment_patch_logic(self): + makes = ["TESLATRUCK", "TESLA", "TEST"] + sales = {"2020":25} + model_year = ModelYear.objects.filter(id=2).first() + model_year_report = ModelYearReport.objects.filter(id=1).first() + data = json.dumps({"makes":makes, "sales":sales}) + + modelYearReportLDVSales1 = ModelYearReportLDVSales.objects.create( + model_year=model_year, + ldv_sales=10, + model_year_report=model_year_report + ) + + response = self.clients['RTAN'].patch("/api/compliance/reports/1/assessment_patch", data=data, content_type='application/json') + + sales_records = ModelYearReportLDVSales.objects.filter( + model_year_id=model_year.id, + model_year_report=model_year_report) + + # Check that second record is created + self.assertEqual(sales_records.count(), 2) + + data = json.dumps({"makes":makes, "sales":{"2020":10}}) + response = self.clients['RTAN'].patch("/api/compliance/reports/1/assessment_patch", data=data, content_type='application/json') + + sales_records = ModelYearReportLDVSales.objects.filter( + model_year_id=model_year.id, + model_year_report=model_year_report) + + # Check for proper deletion of first record + self.assertEqual(sales_records.count(), 1) + + modelYearReportLDVSales2 = ModelYearReportLDVSales.objects.create( + model_year=model_year, + ldv_sales=10, + from_gov=True, + model_year_report=model_year_report + ) + + data = json.dumps({"makes":makes, "sales":{"2020":50}}) + response = self.clients['RTAN'].patch("/api/compliance/reports/1/assessment_patch", data=data, content_type='application/json') + + sales_records = ModelYearReportLDVSales.objects.filter( + model_year_id=model_year.id, + model_year_report=model_year_report) + + sales_record = ModelYearReportLDVSales.objects.filter(id=3).first() + + # check that second record is updated, and no new record created + self.assertEqual(sales_records.count(), 2) + self.assertEqual(sales_record.ldv_sales, 50) diff --git a/backend/api/viewsets/credit_request.py b/backend/api/viewsets/credit_request.py index b734bacd5..9a18ee2dc 100644 --- a/backend/api/viewsets/credit_request.py +++ b/backend/api/viewsets/credit_request.py @@ -8,10 +8,13 @@ from django.db.models import Subquery, Count, Q from django.db.models.expressions import RawSQL from django.http import HttpResponse, HttpResponseForbidden +from api.models.sales_submission_comment import SalesSubmissionComment +from api.serializers.sales_submission_comment import SalesSubmissionCommentSerializer 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.icbc_registration_data import IcbcRegistrationData from api.models.record_of_sale import RecordOfSale @@ -478,3 +481,28 @@ def download_details(self, request, pk): ) ) return response + + @action(detail=True, methods=["PATCH"]) + def update_comment(self, request, pk): + comment_text = request.data.get("comment") + username = request.user.username + comment = SalesSubmissionComment.objects.get( + id=pk + ) + if username == comment.create_user: + serializer = SalesSubmissionCommentSerializer(comment, data={'comment': comment_text}, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + return Response(status=status.HTTP_403_FORBIDDEN) + + @action(detail=True, methods=["PATCH"]) + def delete_comment(self, request, pk): + username = request.user.username + comment = SalesSubmissionComment.objects.get( + id=pk + ) + if username == comment.create_user: + comment.delete() + return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_403_FORBIDDEN) diff --git a/backend/api/viewsets/model_year_report.py b/backend/api/viewsets/model_year_report.py index a73ef3cd4..9d256e634 100644 --- a/backend/api/viewsets/model_year_report.py +++ b/backend/api/viewsets/model_year_report.py @@ -1,7 +1,7 @@ import uuid from django.db.models import Q from django.shortcuts import get_object_or_404 -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseForbidden from rest_framework.response import Response from rest_framework import mixins, viewsets from rest_framework.decorators import action @@ -562,6 +562,32 @@ def comment_save(self, request, pk): return Response(serializer.data) + + @action(detail=True, methods=['patch']) + def comment_patch(self, request, pk): + # only government users can edit comments + if not request.user.is_government: + return HttpResponseForbidden() + + id = request.data.get('id') + comment = request.data.get('comment') + + modelYearReportAssessmentComment = get_object_or_404(ModelYearReportAssessmentComment, pk=id) + + # only the original commenter can edit a comment + if request.user.username != modelYearReportAssessmentComment.create_user: + return HttpResponseForbidden() + + modelYearReportAssessmentComment.comment = comment + modelYearReportAssessmentComment.save() + + report = get_object_or_404(ModelYearReport, pk=pk) + + serializer = ModelYearReportSerializer(report, context={'request': request}) + + return Response(serializer.data) + + @action(detail=True, methods=['get']) def assessment(self, request, pk): report = get_object_or_404(ModelYearReport, pk=pk) diff --git a/backend/api/viewsets/vehicle.py b/backend/api/viewsets/vehicle.py index 1b8a3ae1a..b46942a3d 100644 --- a/backend/api/viewsets/vehicle.py +++ b/backend/api/viewsets/vehicle.py @@ -15,7 +15,8 @@ from api.serializers.vehicle import ModelYearSerializer, \ VehicleZevTypeSerializer, VehicleClassSerializer, \ VehicleSaveSerializer, VehicleSerializer, \ - VehicleStatusChangeSerializer, VehicleIsActiveChangeSerializer + VehicleStatusChangeSerializer, VehicleIsActiveChangeSerializer, \ + VehicleListSerializer from api.services.minio import minio_put_object from auditable.views import AuditableMixin from api.models.vehicle import VehicleDefinitionStatuses @@ -30,6 +31,7 @@ class VehicleViewSet( serializer_classes = { 'default': VehicleSerializer, + 'list': VehicleListSerializer, 'state_change': VehicleStatusChangeSerializer, 'is_active_change': VehicleIsActiveChangeSerializer, 'create': VehicleSaveSerializer, diff --git a/backend/gunicorn.cfg b/backend/gunicorn.cfg deleted file mode 100644 index 32cbf5339..000000000 --- a/backend/gunicorn.cfg +++ /dev/null @@ -1,49 +0,0 @@ -# Gunicorn configuration file. - -# Worker processes -# -# workers - The number of worker processes that this server -# should keep alive for handling requests. -# -# A positive integer generally in the 2-4 x $(NUM_CORES) -# range. You'll want to vary this a bit to find the best -# for your particular application's work load. -# -# worker_class - The type of workers to use. The default -# sync class should handle most 'normal' types of work -# loads. You'll want to read -# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type -# for information on when you might want to choose one -# of the other worker classes. -# -# A string referring to a Python path to a subclass of -# gunicorn.workers.base.Worker. The default provided values -# can be seen at -# http://docs.gunicorn.org/en/latest/settings.html#worker-class -# -# worker_connections - For the eventlet and gevent worker classes -# this limits the maximum number of simultaneous clients that -# a single process can handle. -# -# A positive integer generally set to around 1000. -# -# timeout - If a worker does not notify the master process in this -# number of seconds it is killed and a new worker is spawned -# to replace it. -# -# Generally set to thirty seconds. Only set this noticeably -# higher if you're sure of the repercussions for sync workers. -# For the non sync workers it just means that the worker -# process is still communicating and is not tied to the length -# of time required to handle a single request. -# -# keepalive - The number of seconds to wait for the next request -# on a Keep-Alive HTTP connection. -# -# A positive integer. Generally set in the 1-5 seconds range. -# - -workers = 8 -timeout = 1800 -graceful_timeout = 1800 -keepalive = 5 diff --git a/backend/gunicorn.cfg.py b/backend/gunicorn.cfg.py new file mode 100644 index 000000000..ce8d7e77b --- /dev/null +++ b/backend/gunicorn.cfg.py @@ -0,0 +1,2 @@ +bind = "0.0.0.0:8080" +workers = 2 \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 14be5a8ae..85ee9c349 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,7 +12,7 @@ cryptography==3.4.7 Django==3.1.12 django-celery-beat==1.5.0 django-cors-headers==3.2.1 -django-enumfields==2.0.0 +django-enumfields==2.1.1 django-filter==2.4.0 django-timezone-field==4.0 djangorestframework==3.12.4 diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 000000000..10f9d5722 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +build +coverage + +# Ignore all HTML files: +*.html \ No newline at end of file diff --git a/frontend/.s2i/bin/assemble b/frontend/.s2i/bin/assemble old mode 100644 new mode 100755 index 33bcf8c8a..19faefa24 --- a/frontend/.s2i/bin/assemble +++ b/frontend/.s2i/bin/assemble @@ -1,36 +1,130 @@ +sh-5.1$ cat assemble #!/bin/bash # Prevent running assemble in builders different than official STI image. -# The official nodejs:0.10-onbuild already run npm install and use different +# The official nodejs:8-onbuild already run npm install and use different # application folder. -# if /user/src/app directory exists, quit this script with status 0 [ -d "/usr/src/app" ] && exit 0 -# from "help set", it says "-e Exit immediately if a command exits with a non-zero status." set -e -# there are options which modify the behavior of bash, they can be set or unset using shopt -# -s means If optnames are specified, set those options. If no optnames are specified, list all options that are currently set. -# -u can Unset optnames. -shopt -s dotglob +# 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 -# tfrs/frontend/* were copied at /tmp/src, copy /tmp/src/* into /opt/app-root/src echo "---> Installing application source ..." -cp -r /tmp/src/* ./ && rm -rf /tmp/src/* +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 -echo "---> Building your Node application from source" + #do not fail when there is no build script + echo "---> Building in production mode" + npm run build --if-present -# pull node packages from artifactory -npm cache clean --force -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 + echo "---> Pruning the development dependencies" + npm prune -# -d means --loglevel info -npm install -d + NPM_TMP=$(npm config get tmp) + if ! mountpoint $NPM_TMP; then + echo "---> Cleaning the $NPM_TMP/npm-*" + rm -rf $NPM_TMP/npm-* + fi -# run webpack -npm run dist + # 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/.s2i/bin/run b/frontend/.s2i/bin/run deleted file mode 100644 index b1b930ce3..000000000 --- a/frontend/.s2i/bin/run +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -npm run production \ No newline at end of file diff --git a/frontend/Dockerfile-Openshift b/frontend/Dockerfile-Openshift index 59a2319a8..9aa32a355 100644 --- a/frontend/Dockerfile-Openshift +++ b/frontend/Dockerfile-Openshift @@ -11,4 +11,4 @@ CMD npm run start chmod +x /usr/local/bin/caddy2 # COPY ./Caddyfile /etc/caddy/Caddyfile # RUN chmod +x ./Dockerfile-Openshift-entrypoint.sh -# CMD ["./Dockerfile-Openshift-entrypoint.sh"] +# CMD ["./Dockerfile-Openshift-entrypoint.sh"] \ No newline at end of file diff --git a/frontend/Dockerfile-Openshift-entrypoint.sh b/frontend/Dockerfile-Openshift-entrypoint.sh deleted file mode 100644 index 113f61a5a..000000000 --- a/frontend/Dockerfile-Openshift-entrypoint.sh +++ /dev/null @@ -1,2 +0,0 @@ -nohup caddy2 run --config ./Caddyfile & -npm run start diff --git a/frontend/package.json b/frontend/package.json index 313b5cb17..2f96bcbf7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "zeva-frontend", - "version": "1.41.0", + "version": "1.42.0", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.13.0", diff --git a/frontend/src/app/components/EditComment.js b/frontend/src/app/components/EditComment.js new file mode 100644 index 000000000..41063bbdc --- /dev/null +++ b/frontend/src/app/components/EditComment.js @@ -0,0 +1,62 @@ +import React, { useState, useEffect } from 'react'; +import ReactQuill from 'react-quill'; + +const EditComment = ({ + commentId, + comment, + handleSave, + handleCancel, + handleDelete +}) => { + const [value, setValue] = useState(); + + useEffect(() => { + setValue(comment); + }, [comment]); + + const handleChange = (editedComment) => { + if (editedComment) { + setValue(editedComment); + } + }; + + return ( + <> + + + + + ); +}; + +export default EditComment; diff --git a/frontend/src/app/components/EditableCommentList.js b/frontend/src/app/components/EditableCommentList.js new file mode 100644 index 000000000..ab42ec55e --- /dev/null +++ b/frontend/src/app/components/EditableCommentList.js @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import EditComment from './EditComment'; +import moment from 'moment-timezone'; +import parse from 'html-react-parser'; + +const EditableCommentList = ({ + comments, + user, + handleCommentEdit, + handleCommentDelete +}) => { + const [commentIdsBeingEdited, setCommentIdsBeingEdited] = useState([]); + + const handleEditClick = (commentId) => { + setCommentIdsBeingEdited((prev) => { + return [...prev, commentId]; + }); + }; + + const handleSave = (commentId, commentText) => { + handleCommentEdit(commentId, commentText); + handleCancel(commentId); + }; + + const handleCancel = (commentId) => { + setCommentIdsBeingEdited((prev) => { + return prev.filter((id) => { + return id !== commentId; + }); + }); + }; + + const handleDelete = (commentId) => { + handleCommentDelete(commentId); + handleCancel(commentId); + }; + + const commentElements = []; + for (const comment of comments) { + const commentId = comment.id; + const beingEdited = commentIdsBeingEdited.includes(commentId); + const userEditable = comment.createUser.id == user.id; + if (beingEdited) { + commentElements.push( +
+ +
+ ); + } else { + commentElements.push( +
+ {'Comments - '} + {userEditable && ( + + )} + {comment.createUser.displayName},{' '} + {moment(comment.updateTimestamp).format('YYYY-MM-DD h[:]mm a')} :{' '} + {parse(comment.comment)} +
+
+ ); + } + } + return ( +
+ {commentElements} +
+ ); +}; + +export default EditableCommentList; diff --git a/frontend/src/app/css/App.scss b/frontend/src/app/css/App.scss index 3668faeb4..8e53e46bf 100644 --- a/frontend/src/app/css/App.scss +++ b/frontend/src/app/css/App.scss @@ -360,3 +360,10 @@ p { .ql-editor { height: auto; } + +button { + &.inline-edit { + color: #003366; + border: none; + } +} diff --git a/frontend/src/app/css/Supplementary.scss b/frontend/src/app/css/Supplementary.scss index e579007e4..638a62add 100644 --- a/frontend/src/app/css/Supplementary.scss +++ b/frontend/src/app/css/Supplementary.scss @@ -1,4 +1,3 @@ - #supplementary { .supplementary-form { border: 1px solid $border-grey; @@ -14,6 +13,9 @@ background-color: #e9ecef; opacity: 1; border: 1px solid #ced4da; + &.highlight { + background-color: $background-warning; + } } } } @@ -22,7 +24,7 @@ border: 1px solid $border-grey; padding: 0.5rem; } - + .content { background-color: $default-background-grey; } @@ -34,11 +36,11 @@ } .no-border { - border: none; + border: none; } .credit-selection { - input[type=radio] { + input[type='radio'] { margin-left: 3px; } @@ -59,7 +61,7 @@ table { width: 100%; border: 1px solid $border-grey; - + td { padding: 0 1rem 0 1rem; line-height: 35px; @@ -96,45 +98,51 @@ .supplementary-report-tabs { .nav-item { padding: 1rem 0; - - &.DRAFT, &.UNSAVED, &.SAVED { + + &.DRAFT, + &.UNSAVED, + &.SAVED { a { border-bottom-color: $background-warning; } - + &.active span { border-bottom-color: $primary-yellow; } } - - &.CONFIRMED, &.SUBMITTED, &.RECOMMENDED, &.RETURNED { + + &.CONFIRMED, + &.SUBMITTED, + &.RECOMMENDED, + &.RETURNED { a { - border-bottom-color: $background-light-blue; + border-bottom-color: $background-light-blue; } - + &.active span { border-bottom-color: $default-text-blue; } } - + &.ASSESSED { a { border-bottom-color: $background-light-green; } - + &.active span { border-bottom-color: $alert-success; } } - - a, span { + + a, + span { border-bottom: 1rem transparent solid; color: $default-text-black; display: block; } - + .disabled { - opacity: .3; + opacity: 0.3; } } } @@ -142,4 +150,4 @@ .highlight.form-control { background-color: $background-warning !important; } -} \ No newline at end of file +} diff --git a/frontend/src/app/routes/Compliance.js b/frontend/src/app/routes/Compliance.js index ec8088267..262b695e9 100644 --- a/frontend/src/app/routes/Compliance.js +++ b/frontend/src/app/routes/Compliance.js @@ -22,6 +22,7 @@ const COMPLIANCE = { REPORT_SUMMARY_CONFIRMATION: `${API_BASE_PATH}/reports/:id/submission_confirmation`, REPORT_SUBMISSION: `${API_BASE_PATH}/reports/submission`, ASSESSMENT_COMMENT_SAVE: `${API_BASE_PATH}/reports/:id/comment_save`, + ASSESSMENT_COMMENT_PATCH: `${API_BASE_PATH}/reports/:id/comment_patch`, YEARS: `${API_BASE_PATH}/reports/years`, MAKES: `${API_BASE_PATH}/reports/:id/makes`, SUPPLEMENTAL_CREATE: `${API_BASE_PATH}/reports/:id/supplemental_save`, diff --git a/frontend/src/app/routes/CreditRequests.js b/frontend/src/app/routes/CreditRequests.js index 5f9e4f44a..c8fa932e4 100644 --- a/frontend/src/app/routes/CreditRequests.js +++ b/frontend/src/app/routes/CreditRequests.js @@ -16,7 +16,9 @@ const CREDIT_REQUESTS = { UPLOAD: `${API_BASE_PATH}/upload`, VALIDATE: `${API_BASE_PATH}/:id/validate`, VALIDATED: `${API_BASE_PATH}/:id/validated`, - VALIDATED_DETAILS: `${API_BASE_PATH}/:id/validated-details` + VALIDATED_DETAILS: `${API_BASE_PATH}/:id/validated-details`, + UPDATE_COMMENT: `${API_BASE_PATH}/:id/update_comment`, + DELETE_COMMENT: `${API_BASE_PATH}/:id/delete_comment` }; export default CREDIT_REQUESTS; diff --git a/frontend/src/compliance/AssessmentContainer.js b/frontend/src/compliance/AssessmentContainer.js index b502adce8..9dc5347d8 100644 --- a/frontend/src/compliance/AssessmentContainer.js +++ b/frontend/src/compliance/AssessmentContainer.js @@ -85,6 +85,32 @@ const AssessmentContainer = (props) => { history.replace(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)); }); }; + + const handleAddBceidComment = () => { + const comment = { comment: bceidComment, director: false }; + axios + .post( + ROUTES_COMPLIANCE.ASSESSMENT_COMMENT_SAVE.replace(':id', id), + comment + ) + .then(() => { + history.push(ROUTES_COMPLIANCE.REPORTS); + history.replace(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)); + }); + }; + + const handleEditComment = (comment) => { + axios + .patch( + ROUTES_COMPLIANCE.ASSESSMENT_COMMENT_PATCH.replace(':id', id), + comment + ) + .then(() => { + history.push(ROUTES_COMPLIANCE.REPORTS); + history.replace(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)); + }); + }; + const refreshDetails = () => { if (id) { axios @@ -485,19 +511,6 @@ const AssessmentContainer = (props) => { }); }; - const handleAddBceidComment = () => { - const comment = { comment: bceidComment, director: false }; - axios - .post( - ROUTES_COMPLIANCE.ASSESSMENT_COMMENT_SAVE.replace(':id', id), - comment - ) - .then(() => { - history.push(ROUTES_COMPLIANCE.REPORTS); - history.replace(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)); - }); - }; - return ( <> { handleAddIdirComment={handleAddIdirComment} handleCommentChangeBceid={handleCommentChangeBceid} handleCommentChangeIdir={handleCommentChangeIdir} + handleEditComment={handleEditComment} handleSubmit={handleSubmit} id={id} loading={loading} diff --git a/frontend/src/compliance/components/AssessmentDetailsPage.js b/frontend/src/compliance/components/AssessmentDetailsPage.js index 741339ff8..a431836a3 100644 --- a/frontend/src/compliance/components/AssessmentDetailsPage.js +++ b/frontend/src/compliance/components/AssessmentDetailsPage.js @@ -15,9 +15,10 @@ import formatNumeric from '../../app/utilities/formatNumeric'; import ComplianceObligationReductionOffsetTable from './ComplianceObligationReductionOffsetTable'; import ComplianceObligationTableCreditsIssued from './ComplianceObligationTableCreditsIssued'; import CommentInput from '../../app/components/CommentInput'; -import DisplayComment from '../../app/components/DisplayComment'; import ROUTES_SUPPLEMENTARY from '../../app/routes/SupplementaryReport'; import ComplianceHistory from './ComplianceHistory'; +import AssessmentEditableCommentList from './AssessmentEditableCommentList'; +import AssessmentEditableCommentInput from './AssessmentEditableCommentInput'; const AssessmentDetailsPage = (props) => { const { @@ -28,6 +29,7 @@ const AssessmentDetailsPage = (props) => { handleAddIdirComment, handleCommentChangeBceid, handleCommentChangeIdir, + handleEditComment, loading, makes, reportYear, @@ -55,6 +57,9 @@ const AssessmentDetailsPage = (props) => { const [showModal, setShowModal] = useState(false); const [showModalAssess, setShowModalAssess] = useState(false); + const [editableComment, setEditableComment] = useState(null); + const [editText, setEditText] = useState(''); + const formattedPenalty = formatNumeric( details.assessment.assessmentPenalty, 0 @@ -178,6 +183,33 @@ const AssessmentDetailsPage = (props) => { } }; + const editComment = (comment) => { + const text = comment.comment; + setEditableComment(comment); + setEditText(text); + handleCommentChangeBceid(text); + handleCommentChangeIdir(text); + }; + + const updateEditableCommentText = (text) => { + setEditText(text); + handleCommentChangeBceid(text); + handleCommentChangeIdir(text); + }; + + const saveEditableComment = () => { + let comment = editableComment; + comment.comment = editText; + handleEditComment(comment); + }; + + const cancelEditableComment = () => { + setEditText(''); + setEditableComment(null); + handleCommentChangeIdir(''); + handleCommentChangeBceid(''); + }; + return (
{ (Object.keys(details.changelog.makesAdditions) || details.changelog.ldvChanges > 0) && ( <> -

Assessment Adjustments

+

Internal Record of Assessment

The analyst made the following adjustments: {details.changelog.makesAdditions && ( @@ -253,18 +285,30 @@ const AssessmentDetailsPage = (props) => { {details.idirComment && details.idirComment.length > 0 && user.isGovernment && ( - + )} {statuses.assessment.status !== 'ASSESSED' && ( - )}
@@ -663,6 +707,7 @@ AssessmentDetailsPage.propTypes = { handleAddIdirComment: PropTypes.func.isRequired, handleCommentChangeBceid: PropTypes.func.isRequired, handleCommentChangeIdir: PropTypes.func.isRequired, + handleEditComment: PropTypes.func.isRequired, radioDescriptions: PropTypes.arrayOf(PropTypes.shape()).isRequired, setDetails: PropTypes.func.isRequired, classAReductions: PropTypes.arrayOf(PropTypes.shape()).isRequired, diff --git a/frontend/src/compliance/components/AssessmentEditableCommentInput.js b/frontend/src/compliance/components/AssessmentEditableCommentInput.js new file mode 100644 index 000000000..5d70e336f --- /dev/null +++ b/frontend/src/compliance/components/AssessmentEditableCommentInput.js @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactQuill from 'react-quill'; +import ReactTooltip from 'react-tooltip'; + +const AssessmentEditableCommentInput = (props) => { + const { + handleAddComment, + handleCommentChange, + saveEditableComment, + cancelEditableComment, + editing, + title, + buttonText, + disable, + buttonDisable, + tooltip, + value + } = props; + return ( +
+ + + {!disable && buttonText && ( + <> + {tooltip !== '' && buttonDisable && } + + + + {editing && ( + + )} + + + )} +
+ ); +}; + +AssessmentEditableCommentInput.defaultProps = { + buttonText: null, + editing: false, + disable: false, + handleAddComment: () => {}, + tooltip: '' +}; +AssessmentEditableCommentInput.propTypes = { + handleAddComment: PropTypes.func, + handleCommentChange: PropTypes.func.isRequired, + saveEditableComment: PropTypes.func.isRequired, + cancelEditableComment: PropTypes.func.isRequired, + buttonText: PropTypes.string, + title: PropTypes.string.isRequired, + disable: PropTypes.bool, + tooltip: PropTypes.string +}; +export default AssessmentEditableCommentInput; diff --git a/frontend/src/compliance/components/AssessmentEditableCommentList.js b/frontend/src/compliance/components/AssessmentEditableCommentList.js new file mode 100644 index 000000000..f375a3acf --- /dev/null +++ b/frontend/src/compliance/components/AssessmentEditableCommentList.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment-timezone'; +import parse from 'html-react-parser'; +import { Link } from 'react-router-dom'; + +const AssessmentEditableCommentList = (props) => { + const { commentArray, editComment, user } = props; + return ( +
+ + {commentArray && + commentArray.map((each) => { + const comment = + typeof each.comment === 'string' ? each : each.comment; + const userEditable = user.id == each.createUser.id; + return ( +
+ {'Comments '} + {!userEditable && } + {userEditable && ( + + )} + {each.createUser.displayName},{' '} + {moment(each.createTimestamp).format('YYYY-MM-DD h[:]mm a')} :{' '} + {parse(each.comment)} +
+
+ ); + })} +
+
+ ); +}; + +AssessmentEditableCommentList.defaultProps = { + commentArray: [] +}; +AssessmentEditableCommentList.propTypes = { + commentArray: PropTypes.arrayOf(PropTypes.shape()), + editComment: PropTypes.func.isRequired +}; +export default AssessmentEditableCommentList; diff --git a/frontend/src/compliance/components/ComplianceObligationDetailsPage.js b/frontend/src/compliance/components/ComplianceObligationDetailsPage.js index 455771025..6fd487223 100644 --- a/frontend/src/compliance/components/ComplianceObligationDetailsPage.js +++ b/frontend/src/compliance/components/ComplianceObligationDetailsPage.js @@ -169,49 +169,58 @@ const ComplianceObligationDetailsPage = (props) => { user={user} />
- -
-
-
- -
+
- - + + )} {modal} ); diff --git a/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js b/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js index b60e84b2c..f3c0f13f3 100644 --- a/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js +++ b/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js @@ -229,46 +229,54 @@ const ComplianceReportSummaryDetailsPage = (props) => { + {['SUBMITTED', 'ASSESSED', 'REASSESSED'].indexOf( + confirmationStatuses.reportSummary.status + ) == -1 && ( + <> +
+
+ +
+
-
-
- -
-
- -
-
-
- -
+
- - + + )} {modal} ); diff --git a/frontend/src/compliance/components/ComplianceReportsTable.js b/frontend/src/compliance/components/ComplianceReportsTable.js index 0983f68f3..c2b94d695 100644 --- a/frontend/src/compliance/components/ComplianceReportsTable.js +++ b/frontend/src/compliance/components/ComplianceReportsTable.js @@ -175,63 +175,36 @@ const ComplianceReportsTable = (props) => { supplementalStatus } = row.original; - if (supplementalStatus === 'REASSESSED' && supplementalId) { - history.push( - ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace( - /:id/g, - id - ).replace(/:supplementaryId/g, supplementalId) - ); - } else if ( + if ( supplementalStatus === 'SUPPLEMENTARY SUBMITTED' && - supplementalId - ) { - if (user.isGovernment) { - history.push({ - pathname: - ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace( - /:id/g, - id - ).replace(/:supplementaryId/g, supplementalId), - search: '?reassessment=Y' - }); - } else { - history.push( - ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace( - /:id/g, - id - ).replace(/:supplementaryId/g, supplementalId) - ); - } - } else if ( - ['REASSESSMENT DRAFT', 'REASSESSMENT RECOMMENDED'].indexOf( - supplementalStatus - ) >= 0 && supplementalId && user.isGovernment ) { - history.push( - ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace( + history.push({ + pathname: ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace( /:id/g, id - ).replace(/:supplementaryId/g, supplementalId) - ); - } else if (validationStatus === 'ASSESSED') { - history.push( - ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(/:id/g, id) - ); - } else if (supplementalId) { + ).replace(/:supplementaryId/g, supplementalId), + search: '?reassessment=Y' + }); + return; + } + + if (supplementalId) { + // Shows latest supplementary report if one exists history.push( ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace( /:id/g, id ).replace(/:supplementaryId/g, supplementalId) ); - } else if (user.isGovernment) { + } else if (validationStatus === 'ASSESSED') { + // If there is no supplementary report then we default to the first myr history.push( ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(/:id/g, id) ); } else { + // Default show the supplier information page history.push( ROUTES_COMPLIANCE.REPORT_SUPPLIER_INFORMATION.replace( /:id/g, diff --git a/frontend/src/compliance/components/ConsumerSalesDetailsPage.js b/frontend/src/compliance/components/ConsumerSalesDetailsPage.js index 24b116cc0..701d39835 100644 --- a/frontend/src/compliance/components/ConsumerSalesDetailsPage.js +++ b/frontend/src/compliance/components/ConsumerSalesDetailsPage.js @@ -166,53 +166,65 @@ const ConsumerSalesDetailsPage = (props) => { -
-
- -
-
- -
-
-
- -
-
- + +
+
+
+ +
+
+
+ + )} {modal} ); diff --git a/frontend/src/compliance/components/SupplierInformationDetailsPage.js b/frontend/src/compliance/components/SupplierInformationDetailsPage.js index 2c5da789b..ad0d12fc9 100644 --- a/frontend/src/compliance/components/SupplierInformationDetailsPage.js +++ b/frontend/src/compliance/components/SupplierInformationDetailsPage.js @@ -298,57 +298,71 @@ const SupplierInformationDetailsPage = (props) => { - -
-
- -
-
- -
-
-
- -
-
- + +
+
+
+ +
+
+
+ + )} + {modal} ); diff --git a/frontend/src/creditagreements/components/CreditAgreementsFilter.js b/frontend/src/creditagreements/components/CreditAgreementsFilter.js index 982b47a1b..68a1c5f09 100644 --- a/frontend/src/creditagreements/components/CreditAgreementsFilter.js +++ b/frontend/src/creditagreements/components/CreditAgreementsFilter.js @@ -8,8 +8,8 @@ import handleFilterChange from '../../app/utilities/handleFilterChange'; const CreditAgreementsFilter = (props) => { const { user, items, handleClear, filtered, setFiltered } = props; - const isDirector = user.isGovernment - && user.roles.some(r => r.roleCode === 'Director'); + const isDirector = + user.isGovernment && user.roles.some((r) => r.roleCode === 'Director'); const handleChange = (event) => { setFiltered(handleFilterChange(event, filtered)); }; diff --git a/frontend/src/creditagreements/components/CreditAgreementsForm.js b/frontend/src/creditagreements/components/CreditAgreementsForm.js index d3d6539a6..adb471595 100644 --- a/frontend/src/creditagreements/components/CreditAgreementsForm.js +++ b/frontend/src/creditagreements/components/CreditAgreementsForm.js @@ -54,7 +54,10 @@ const CreditAgreementsForm = (props) => { agreementDetails.transactionType === 'Reassessment Reduction') ) { for (const modelYearReport of modelYearReports) { - if (modelYearReport.organizationId == parseInt(agreementDetails.vehicleSupplier)) { + if ( + modelYearReport.organizationId == + parseInt(agreementDetails.vehicleSupplier) + ) { supplierReports.push(modelYearReport); } } @@ -174,7 +177,11 @@ const CreditAgreementsForm = (props) => { dropdownData={suppliers} dropdownName="Vehicle Supplier" handleInputChange={(event) => { - handleChangeDetails(event.target.value, 'vehicleSupplier', true); + handleChangeDetails( + event.target.value, + 'vehicleSupplier', + true + ); }} fieldName="vehicleSupplier" accessor={(supplier) => supplier.id} @@ -189,7 +196,11 @@ const CreditAgreementsForm = (props) => { dropdownData={transactionTypes} dropdownName="Transaction Type" handleInputChange={(event) => { - handleChangeDetails(event.target.value, 'transactionType', true); + handleChangeDetails( + event.target.value, + 'transactionType', + true + ); }} fieldName="transactionType" accessor={(transactionType) => transactionType.name} diff --git a/frontend/src/credits/CreditRequestDetailsContainer.js b/frontend/src/credits/CreditRequestDetailsContainer.js index dce8df566..39a311d3e 100644 --- a/frontend/src/credits/CreditRequestDetailsContainer.js +++ b/frontend/src/credits/CreditRequestDetailsContainer.js @@ -84,6 +84,56 @@ const CreditRequestDetailsContainer = (props) => { }); }; + const handleInternalCommentEdit = (commentId, commentText) => { + axios + .patch(ROUTES_CREDIT_REQUESTS.UPDATE_COMMENT.replace(':id', commentId), { + comment: commentText + }) + .then((response) => { + const updatedComment = response.data; + setSubmission((prev) => { + const commentIndex = prev.salesSubmissionComment.findIndex( + (comment) => { + return comment.id === updatedComment.id; + } + ); + const comment = prev.salesSubmissionComment[commentIndex]; + const commentCopy = { ...comment }; + commentCopy.comment = updatedComment.comment; + commentCopy.updateTimestamp = updatedComment.updateTimestamp; + + const comments = prev.salesSubmissionComment; + const commentsCopy = [...comments]; + commentsCopy[commentIndex] = commentCopy; + + const submissionCopy = { ...prev }; + submissionCopy.salesSubmissionComment = commentsCopy; + return submissionCopy; + }); + }); + }; + + const handleInternalCommentDelete = (commentId) => { + axios + .patch(ROUTES_CREDIT_REQUESTS.DELETE_COMMENT.replace(':id', commentId)) + .then(() => { + setSubmission((prev) => { + const commentIndex = prev.salesSubmissionComment.findIndex( + (comment) => { + return comment.id === commentId; + } + ); + const comments = prev.salesSubmissionComment; + const commentsCopy = [...comments]; + commentsCopy.splice(commentIndex, 1); + + const submissionCopy = { ...prev }; + submissionCopy.salesSubmissionComment = commentsCopy; + return submissionCopy; + }); + }); + }; + if (loading) { return ; } @@ -101,6 +151,8 @@ const CreditRequestDetailsContainer = (props) => { validatedOnly={validatedOnly} handleCheckboxClick={handleCheckboxClick} issueAsMY={issueAsMY} + handleInternalCommentEdit={handleInternalCommentEdit} + handleInternalCommentDelete={handleInternalCommentDelete} /> ]; }; diff --git a/frontend/src/credits/components/CreditRequestDetailsPage.js b/frontend/src/credits/components/CreditRequestDetailsPage.js index c5ff1ddac..447c0ff7d 100644 --- a/frontend/src/credits/components/CreditRequestDetailsPage.js +++ b/frontend/src/credits/components/CreditRequestDetailsPage.js @@ -22,6 +22,7 @@ import getFileSize from '../../app/utilities/getFileSize'; import DisplayComment from '../../app/components/DisplayComment'; import formatNumeric from '../../app/utilities/formatNumeric'; import DownloadAllSubmissionContentButton from './DownloadAllSubmissionContentButton'; +import EditableCommentList from '../../app/components/EditableCommentList'; const CreditRequestDetailsPage = (props) => { const { @@ -30,7 +31,9 @@ const CreditRequestDetailsPage = (props) => { submission, user, issueAsMY, - handleCheckboxClick + handleCheckboxClick, + handleInternalCommentEdit, + handleInternalCommentDelete } = props; const { id } = useParams(); @@ -351,8 +354,11 @@ const CreditRequestDetailsPage = (props) => { user.isGovernment && ( <> Internal Comments - )} diff --git a/frontend/src/credits/components/CreditRequestValidatedDetailsPage.js b/frontend/src/credits/components/CreditRequestValidatedDetailsPage.js index 9925749c3..855e491e4 100644 --- a/frontend/src/credits/components/CreditRequestValidatedDetailsPage.js +++ b/frontend/src/credits/components/CreditRequestValidatedDetailsPage.js @@ -201,6 +201,7 @@ const CreditRequestValidatedDetailsPage = (props) => { setPages={setPages} setReactTable={setReactTable} user={user} + preInitialize={true} /> diff --git a/frontend/src/credits/components/VINListTable.js b/frontend/src/credits/components/VINListTable.js index 20b77f2eb..ed84b34e9 100644 --- a/frontend/src/credits/components/VINListTable.js +++ b/frontend/src/credits/components/VINListTable.js @@ -25,7 +25,8 @@ const VINListTable = (props) => { reasons, refreshContent, setFiltered, - setReactTable + setReactTable, + preInitialize } = props; const [tableInitialized, setTableInitialized] = useState(false); @@ -347,7 +348,9 @@ const VINListTable = (props) => { // onFetchData is called on component load (and on changes afterword) // which we want to avoid, so this tableInitialized // variable cancels out the first call to this method - if (!tableInitialized) { + if(!tableInitialized && preInitialize) { + setTableInitialized(true); + } else if (!tableInitialized) { setTableInitialized(true); return; } diff --git a/frontend/src/supplementary/components/SupplementaryDetailsPage.js b/frontend/src/supplementary/components/SupplementaryDetailsPage.js index a2a43948d..07c9ee2cd 100644 --- a/frontend/src/supplementary/components/SupplementaryDetailsPage.js +++ b/frontend/src/supplementary/components/SupplementaryDetailsPage.js @@ -676,11 +676,14 @@ const SupplementaryDetailsPage = (props) => { (isEditable || ['SUBMITTED', 'RECOMMENDED'].indexOf(details.status) >= 0) && user.isGovernment && - ((currentStatus === 'SUBMITTED' && + (((currentStatus === 'SUBMITTED' || + currentStatus === 'RETURNED') && + analystAction && details && details.reassessment && !details.reassessment.isReassessment) || (currentStatus === 'RECOMMENDED' && + directorAction && details && details.reassessment && details.reassessment.isReassessment)) && ( @@ -695,7 +698,8 @@ const SupplementaryDetailsPage = (props) => { }} type="button" > - {currentStatus === 'SUBMITTED' + {currentStatus === 'SUBMITTED' || + currentStatus === 'RETURNED' ? 'Return to Vehicle Supplier' : 'Return to Analyst'} diff --git a/frontend/src/vehicles/components/VehicleListTable.js b/frontend/src/vehicles/components/VehicleListTable.js index 84a28a640..251dee899 100644 --- a/frontend/src/vehicles/components/VehicleListTable.js +++ b/frontend/src/vehicles/components/VehicleListTable.js @@ -54,7 +54,7 @@ const VehicleListTable = (props) => { width: 125 }, { - accessor: (row) => (row.modelYear ? row.modelYear.name : ''), + accessor: (row) => row.modelYear, className: 'text-center', Header: 'Model Year', id: 'col-my', @@ -80,7 +80,7 @@ const VehicleListTable = (props) => { width: 125 }, { - accessor: (row) => row.vehicleZevType.vehicleZevCode, + accessor: (row) => row.vehicleZevType, className: 'text-center', Header: 'ZEV Type', id: 'zev-type', diff --git a/openshift/templates/backend/backend-bc.yaml b/openshift/templates/backend/backend-bc.yaml index 6f145341d..db3f54b4e 100644 --- a/openshift/templates/backend/backend-bc.yaml +++ b/openshift/templates/backend/backend-bc.yaml @@ -98,7 +98,7 @@ objects: key: password from: kind: ImageStreamTag - name: python:3.6-1-134 + name: python-39:1-74 pullSecret: name: zeva-image-pull-secret forcePull: true diff --git a/openshift/templates/backend/backend-dc.yaml b/openshift/templates/backend/backend-dc.yaml index 6108eeac0..c038a7641 100644 --- a/openshift/templates/backend/backend-dc.yaml +++ b/openshift/templates/backend/backend-dc.yaml @@ -168,7 +168,7 @@ objects: - /bin/sh - '-c' - |- - sleep 90 + sleep 45 python ./manage.py migrate if [ $? -eq 0 ]; then python ./manage.py load_ops_data --directory ./api/fixtures/operational @@ -314,7 +314,7 @@ objects: name: ${NAME}-config${SUFFIX} key: rabbitmq_port - name: APP_CONFIG - value: /opt/app-root/src/gunicorn.cfg + value: /opt/app-root/src/gunicorn.cfg.py - name: ENV_NAME valueFrom: configMapKeyRef: @@ -362,19 +362,11 @@ objects: secretKeyRef: name: email-service key: SENDER_EMAIL - livenessProbe: - failureThreshold: 30 - tcpSocket: - port: 8080 - initialDelaySeconds: ${{HEALTH_CHECK_DELAY}} - periodSeconds: 15 - successThreshold: 1 - timeoutSeconds: 3 ports: - containerPort: 8080 protocol: TCP readinessProbe: - failureThreshold: 30 + failureThreshold: 90 tcpSocket: port: 8080 initialDelaySeconds: ${{HEALTH_CHECK_DELAY}} diff --git a/openshift/templates/frontend/frontend-bc.yaml b/openshift/templates/frontend/frontend-bc.yaml index b03b2c36d..a3374b8f9 100644 --- a/openshift/templates/frontend/frontend-bc.yaml +++ b/openshift/templates/frontend/frontend-bc.yaml @@ -58,10 +58,10 @@ objects: resources: limits: cpu: 2000m - memory: 2G + memory: 4Gi requests: - cpu: 500m - memory: 200M + cpu: 1000m + memory: 2Gi runPolicy: Serial source: git: @@ -73,7 +73,7 @@ objects: sourceStrategy: from: kind: ImageStreamTag - name: nodejs:14-1-28 + name: nodejs-16:1-59 namespace: e52f12-tools pullSecret: name: zeva-image-pull-secret diff --git a/openshift/templates/rabbitmq/rabbitmq-secret-configmap-only.yaml b/openshift/templates/rabbitmq/rabbitmq-secret-configmap-only.yaml index 22feae87f..e767730f3 100644 --- a/openshift/templates/rabbitmq/rabbitmq-secret-configmap-only.yaml +++ b/openshift/templates/rabbitmq/rabbitmq-secret-configmap-only.yaml @@ -1,4 +1,4 @@ -apiVersion: v1 +apiVersion: template.openshift.io/v1 kind: Template metadata: name: rabbitmq-cluster