From 16cb7dffb6c6d0f486ba1a3122551fe0f5502c2a Mon Sep 17 00:00:00 2001 From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com> Date: Thu, 10 Dec 2020 13:59:43 -0800 Subject: [PATCH] Tracking pull request to merge release-1.15.0.1 to master (#424) Fix site slowness issue, update configmap mount location on frontend and correct prod logoff url --- .pipeline-v3/lib/config.js | 6 +- .pipeline-v3/lib/deploy.js | 3 +- .pipeline/lib/config.js | 4 +- .yo-rc.json | 16 +- .yo-rc.json-v4 | 54 ------- backend/api/serializers/credit_transfer.py | 7 +- backend/api/serializers/sales_submission.py | 111 +++++++++---- backend/api/services/credit_transaction.py | 26 ++- backend/api/services/sales_spreadsheet.py | 8 +- backend/api/tests/test_credit_transfers.py | 98 +++++++++++- backend/api/tests/test_vehicles.py | 21 +++ backend/api/viewsets/credit_request.py | 123 ++++++++++++++- frontend/package.json | 2 +- frontend/src/app/routes/CreditRequests.js | 2 + .../credits/CreditRequestVINListContainer.js | 52 +++--- .../credits/UploadCreditRequestContainer.js | 9 ++ .../components/CreditRequestDetailsPage.js | 8 +- .../components/CreditRequestListTable.js | 4 +- .../components/CreditRequestVINListPage.js | 21 ++- .../components/CreditTransfersDetailsPage.js | 10 +- .../credits/components/SubmissionListTable.js | 149 ------------------ .../src/credits/components/VINListTable.js | 78 +++++++-- openshift-v3/templates/backend/README.md | 3 + .../templates/backend/backend-dc.yaml | 30 ++-- .../backend/email-service-secret.yaml | 26 +++ openshift-v3/templates/config/configmap.yaml | 10 +- .../templates/frontend/frontend-dc.yaml | 2 +- openshift-v3/templates/nagios/nagios-dc.yaml | 2 - openshift-v3/templates/patroni/README.md | 2 +- .../unittest/backend-dc-unittest.yaml | 10 -- openshift/templates/backend/README.md | 3 + openshift/templates/backend/backend-dc.yaml | 30 ++-- .../backend/email-service-secret.yaml | 26 +++ openshift/templates/config/configmap.yaml | 4 +- openshift/templates/frontend/frontend-dc.yaml | 2 +- openshift/templates/nagios/nagios-dc.yaml | 2 - .../commands/check_email_connection.sh | 19 ++- openshift/templates/patroni/README.md | 2 +- .../unittest/backend-dc-unittest.yaml | 10 -- 39 files changed, 612 insertions(+), 383 deletions(-) delete mode 100644 .yo-rc.json-v4 delete mode 100644 frontend/src/credits/components/SubmissionListTable.js create mode 100644 openshift-v3/templates/backend/email-service-secret.yaml create mode 100644 openshift/templates/backend/email-service-secret.yaml diff --git a/.pipeline-v3/lib/config.js b/.pipeline-v3/lib/config.js index 627408f65..17c863624 100644 --- a/.pipeline-v3/lib/config.js +++ b/.pipeline-v3/lib/config.js @@ -14,7 +14,7 @@ const phases = { dev: {namespace:'tbiwaq-dev', transient:true, name: `${name}`, ssoSuffix:'-dev', ssoName:'dev.oidc.gov.bc.ca', phase: 'dev' , changeId:`${changeId}`, suffix: `-dev-${changeId}`, instance: `${name}-dev-${changeId}` , version:`${version}-${changeId}`, tag:`dev-${version}-${changeId}`, - host: `zeva-dev-${changeId}.${ocpName}.gov.bc.ca`, djangoDebug: 'True', + host: `zeva-dev-${changeId}.${ocpName}.gov.bc.ca`, djangoDebug: 'True', logoutHost: 'logontest.gov.bc.ca', frontendCpuRequest: '100m', frontendCpuLimit: '700m', frontendMemoryRequest: '300M', frontendMemoryLimit: '4G', frontendReplicas: 1, backendCpuRequest: '300m', backendCpuLimit: '600m', backendMemoryRequest: '1G', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendHost: `zeva-backend-dev-${changeId}.${ocpName}.gov.bc.ca`, backendReplicas: 1, minioCpuRequest: '100m', minioCpuLimit: '200m', minioMemoryRequest: '200M', minioMemoryLimit: '500M', minioPvcSize: '1G', @@ -25,7 +25,7 @@ const phases = { test: {namespace:'tbiwaq-test', name: `${name}`, ssoSuffix:'-test', ssoName:'test.oidc.gov.bc.ca', phase: 'test' , changeId:`${changeId}`, suffix: `-test`, instance: `${name}-test`, version:`${version}`, tag:`test-${version}`, - host: `zeva-test.${ocpName}.gov.bc.ca`, djangoDebug: 'False', + host: `zeva-test.${ocpName}.gov.bc.ca`, djangoDebug: 'False', logoutHost: 'logontest.gov.bc.ca', frontendCpuRequest: '300m', frontendCpuLimit: '600m', frontendMemoryRequest: '500M', frontendMemoryLimit: '1G', frontendReplicas: 2, frontendMinReplicas: 2, frontendMaxReplicas: 5, backendCpuRequest: '100m', backendCpuLimit: '500m', backendMemoryRequest: '500M', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendReplicas: 1, backendMinReplicas: 2, backendMaxReplicas: 5, backendHost: `zeva-backend-test.${ocpName}.gov.bc.ca`, minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '5G', @@ -36,7 +36,7 @@ const phases = { prod: {namespace:'tbiwaq-prod', name: `${name}`, ssoSuffix:'', ssoName:'oidc.gov.bc.ca', phase: 'prod' , changeId:`${changeId}`, suffix: `-prod`, instance: `${name}-prod`, version:`${version}`, tag:`prod-${version}`, - host: `zeroemissionvehicles.${ocpName}.gov.bc.ca`, djangoDebug: 'False', + host: `zeroemissionvehicles.${ocpName}.gov.bc.ca`, djangoDebug: 'False', logoutHost: 'logon7.gov.bc.ca', frontendCpuRequest: '300m', frontendCpuLimit: '600m', frontendMemoryRequest: '1G', frontendMemoryLimit: '2G', frontendReplicas: 2, frontendMinReplicas: 2, frontendMaxReplicas: 5, backendCpuRequest: '200m', backendCpuLimit: '700m', backendMemoryRequest: '1G', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendReplicas: 1, backendMinReplicas: 2, backendMaxReplicas: 5, backendHost: `zeva-backend-prod.${ocpName}.gov.bc.ca`, minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '10G', diff --git a/.pipeline-v3/lib/deploy.js b/.pipeline-v3/lib/deploy.js index f268b1f2e..9c1445536 100755 --- a/.pipeline-v3/lib/deploy.js +++ b/.pipeline-v3/lib/deploy.js @@ -32,7 +32,8 @@ module.exports = settings => { 'SSO_NAME': phases[phase].ssoName, 'KEYCLOAK_REALM': 'rzh2zkjq', 'DJANGO_DEBUG': phases[phase].djangoDebug, - 'OCP_NAME': phases[phase].ocpName + 'OCP_NAME': phases[phase].ocpName, + 'LOGOUT_HOST': phases[phase].logoutHost } })) diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index b2016ef05..979a29e60 100644 --- a/.pipeline/lib/config.js +++ b/.pipeline/lib/config.js @@ -31,7 +31,7 @@ const phases = { minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '5G', schemaspyCpuRequest: '20m', schemaspyCpuLimit: '200m', schemaspyMemoryRequest: '150M', schemaspyMemoryLimit: '300M', schemaspyHealthCheckDelay: 160, rabbitmqCpuRequest: '250m', rabbitmqCpuLimit: '700m', rabbitmqMemoryRequest: '500M', rabbitmqMemoryLimit: '700M', rabbitmqPvcSize: '1G', rabbitmqReplica: 2, rabbitmqPostStartSleep: 120, storageClass: 'netapp-block-standard', - patroniCpuRequest: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '500M', patroniMemoryLimit: '1G', patroniPvcSize: '40G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`}, + patroniCpuRequest: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '500M', patroniMemoryLimit: '1G', patroniPvcSize: '5G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`}, prod: {namespace:'e52f12-prod', name: `${name}`, ssoSuffix:'', ssoName:'oidc.gov.bc.ca', phase: 'prod' , changeId:`${changeId}`, suffix: `-prod`, @@ -42,7 +42,7 @@ const phases = { minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '10G', schemaspyCpuRequest: '50m', schemaspyCpuLimit: '400m', schemaspyMemoryRequest: '150M', schemaspyMemoryLimit: '300M', schemaspyHealthCheckDelay: 160, rabbitmqCpuRequest: '250m', rabbitmqCpuLimit: '700m', rabbitmqMemoryRequest: '500M', rabbitmqMemoryLimit: '1G', rabbitmqPvcSize: '5G', rabbitmqReplica: 2, rabbitmqPostStartSleep: 120, storageClass: 'netapp-block-standard', - patroniCpuRequest: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '1G', patroniMemoryLimit: '2G', patroniPvcSize: '50G', patroniReplica: 3, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`}, + patroniCpuRequest: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '1G', patroniMemoryLimit: '2G', patroniPvcSize: '30G', patroniReplica: 3, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`}, }; diff --git a/.yo-rc.json b/.yo-rc.json index b592ba595..6c663d39e 100644 --- a/.yo-rc.json +++ b/.yo-rc.json @@ -8,16 +8,16 @@ "path": ".", "environments": { "build": { - "namespace": "tbiwaq-tools" + "namespace": "f52f12-tools" }, "dev": { - "namespace": "tbiwaq-dev" + "namespace": "f52f12-dev" }, "test": { - "namespace": "tbiwaq-test" + "namespace": "f52f12-test" }, "prod": { - "namespace": "tbiwaq-prod" + "namespace": "f52f12-prod" } }, "jenkinsJobName": "zeva", @@ -29,17 +29,17 @@ "jenkins": { "path": ".jenkins", "name": "jenkins", - "namespace": "tbiwaq-tools", + "namespace": "f52f12-tools", "version": "1.0.0", "environments": { "build": { - "namespace": "tbiwaq-tools" + "namespace": "f52f12-tools" }, "dev": { - "namespace": "tbiwaq-tools" + "namespace": "f52f12-tools" }, "prod": { - "namespace": "tbiwaq-tools" + "namespace": "f52f12-tools" } }, "jenkinsJobName": "_jenkins", diff --git a/.yo-rc.json-v4 b/.yo-rc.json-v4 deleted file mode 100644 index 6c663d39e..000000000 --- a/.yo-rc.json-v4 +++ /dev/null @@ -1,54 +0,0 @@ -{ - "@bcgov/bcdk": { - "promptValues": { - "modules": { - "zeva": { - "name": "zeva", - "version": "1.0.0", - "path": ".", - "environments": { - "build": { - "namespace": "f52f12-tools" - }, - "dev": { - "namespace": "f52f12-dev" - }, - "test": { - "namespace": "f52f12-test" - }, - "prod": { - "namespace": "f52f12-prod" - } - }, - "jenkinsJobName": "zeva", - "github_owner": "bcgov", - "github_repo": "zeva", - "jenkinsFilePath": "Jenkinsfile", - "uuid": "2c0c5c5b-6d52-4ae1-aefb-00b5bbd7c7e3" - }, - "jenkins": { - "path": ".jenkins", - "name": "jenkins", - "namespace": "f52f12-tools", - "version": "1.0.0", - "environments": { - "build": { - "namespace": "f52f12-tools" - }, - "dev": { - "namespace": "f52f12-tools" - }, - "prod": { - "namespace": "f52f12-tools" - } - }, - "jenkinsJobName": "_jenkins", - "github_owner": "bcgov", - "github_repo": "zeva", - "jenkinsFilePath": ".jenkins/Jenkinsfile", - "uuid": "7a55c783-db16-45ad-82cc-23792286d6c3" - } - } - } - } -} \ No newline at end of file diff --git a/backend/api/serializers/credit_transfer.py b/backend/api/serializers/credit_transfer.py index 8ec935d8d..54a39d3c4 100644 --- a/backend/api/serializers/credit_transfer.py +++ b/backend/api/serializers/credit_transfer.py @@ -208,7 +208,12 @@ def create(self, validated_data): signing_authority_assertion_id=confirmation ) - serializer = CreditTransferSerializer(credit_transfer, read_only=True) + serializer = CreditTransferSerializer( + credit_transfer, read_only=True, + context={ + 'request': request + } + ) return serializer.data diff --git a/backend/api/serializers/sales_submission.py b/backend/api/serializers/sales_submission.py index 0e28f0dc6..b9e231877 100644 --- a/backend/api/serializers/sales_submission.py +++ b/backend/api/serializers/sales_submission.py @@ -87,6 +87,27 @@ class SalesSubmissionListSerializer( validation_status = SerializerMethodField() submission_history = SerializerMethodField() + def get_submission_history(self, obj): + request = self.context.get('request') + if not request.user.is_government and obj.validation_status in [ + SalesSubmissionStatuses.RECOMMEND_REJECTION, + SalesSubmissionStatuses.RECOMMEND_APPROVAL, + SalesSubmissionStatuses.CHECKED, + ]: + # return the date that it was submitted + history = SalesSubmissionHistory.objects.filter( + submission_id=obj.id, + validation_status=SalesSubmissionStatuses.SUBMITTED + ).order_by('update_timestamp').first() + return history.update_timestamp.date() + # return the last updated date + history = SalesSubmissionHistory.objects.filter( + submission_id=obj.id, + validation_status=obj.validation_status + ).order_by('update_timestamp').first() + return history.update_timestamp.date() + + def get_submission_history(self, obj): request = self.context.get('request') if not request.user.is_government and obj.validation_status in [ @@ -173,9 +194,7 @@ def get_total_warnings(self, obj): ] if obj.validation_status in valid_statuses: - for row in obj.content.all(): - if len(row.warnings) > 0 and row.record_of_sale is None: - warnings += 1 + warnings = obj.unselected return warnings @@ -192,22 +211,6 @@ class Meta: 'total_credits', 'total_warnings', 'unselected', ) - -class SalesSubmissionHistorySerializer( - ModelSerializer, EnumSupportSerializerMixin, BaseSerializer -): - create_user = SerializerMethodField() - update_user = SerializerMethodField() - validation_status = SerializerMethodField() - - class Meta: - model = SalesSubmissionHistory - fields = ( - 'create_timestamp', 'create_user', - 'validation_status', 'update_user' - ) - - class SalesSubmissionSerializer( ModelSerializer, EnumSupportSerializerMixin, BaseSerializer @@ -353,6 +356,31 @@ class Meta: ) +class SalesSubmissionDetailSerializer( + ModelSerializer, EnumSupportSerializerMixin, + BaseSerializer +): + content = SerializerMethodField() + + def get_content(self, instance): + request = self.context.get('request') + + serializer = SalesSubmissionContentSerializer( + instance.content, + read_only=True, + many=True, + context={'request': request} + ) + + return serializer.data + + class Meta: + model = SalesSubmission + fields = ( + 'id', 'validation_status', 'content', 'submission_id' + ) + + class SalesSubmissionSaveSerializer( ModelSerializer ): @@ -372,7 +400,7 @@ def validate_validation_status(self, value): def update(self, instance, validated_data): request = self.context.get('request') - records = request.data.get('records') + invalidated = request.data.get('invalidated', None) sales_submission_comment = validated_data.pop('sales_submission_comment', None) if sales_submission_comment: @@ -382,15 +410,44 @@ def update(self, instance, validated_data): comment=sales_submission_comment.get('comment') ) - if records is not None: + if invalidated is not None: RecordOfSale.objects.filter(submission_id=instance.id).delete() + valid_vehicles = Vehicle.objects.filter( + organization_id=instance.organization_id, + validation_status=VehicleDefinitionStatuses.VALIDATED + ).values_list('model_year__name', Upper('make'), 'model_name') + + duplicate_vins = SalesSubmissionContent.objects.annotate( + vin_count=Count('xls_vin') + ).filter( + submission_id=instance.id, + vin_count__gt=1 + ).values_list('xls_vin', flat=True) + + awarded_vins = RecordOfSale.objects.exclude( + submission_id=instance.id + ).values_list('vin', flat=True) + + content = SalesSubmissionContent.objects.filter( + submission_id=instance.id + ).exclude( + id__in=invalidated + ).exclude( + xls_vin__in=awarded_vins + ).exclude( + xls_vin__in=duplicate_vins, + xls_sale_date__lte="43102.0" + ) - for record_id in records: - row = SalesSubmissionContent.objects.filter( - id=record_id - ).first() + for row in content: + try: + model_year = int(float(row.xls_model_year)) + except ValueError: + model_year = 0 - if row and row.vehicle: + if ( + str(model_year), row.xls_make.upper(), row.xls_model, + ) in valid_vehicles: RecordOfSale.objects.create( sale_date=get_date( row.xls_sale_date, @@ -408,7 +465,7 @@ def update(self, instance, validated_data): if validation_status: SalesSubmissionHistory.objects.create( - submission=instance, + submission_id=instance.id, validation_status=validation_status, update_user=request.user.username, create_user=request.user.username, diff --git a/backend/api/services/credit_transaction.py b/backend/api/services/credit_transaction.py index e24a5b92d..47690f583 100644 --- a/backend/api/services/credit_transaction.py +++ b/backend/api/services/credit_transaction.py @@ -13,6 +13,8 @@ from api.models.vehicle import Vehicle from api.models.weight_class import WeightClass from api.services.credit_transfer import aggregate_credit_transfer_details +from django.core.exceptions import ValidationError + def award_credits(submission): records = RecordOfSale.objects.filter( @@ -182,19 +184,17 @@ def validate_transfer(transfer): weight_type = each.weight_class.id # check if supplier has enough for this transfer for record in supplier_totals: - if (record['model_year_id'] == model_year and record['credit_class_id'] == credit_type - and record['weight_class_id'] == weight_type): - print(record['total_value']) - found = True - record['total_value'] -= credit_value - print(record) - if record['total_value'] < 0: - print('NOT ENOUGH CREDITS') - has_enough = False + if (record['model_year_id'] == model_year and record[ + 'credit_class_id'] == credit_type + and record['weight_class_id'] == weight_type): + found = True + record['total_value'] -= credit_value + if record['total_value'] < 0: + has_enough = False if not found: has_enough = False if not has_enough: - raise Exception('not enough credits') + raise ValidationError('not enough credits') else: # add to each dictionary (one broken down by years and the other not) if credit_type not in credit_total_no_years: @@ -223,7 +223,8 @@ def validate_transfer(transfer): ), total_value=1 * credit_value, update_user=transfer.update_user, - weight_class_id=1 + weight_class=WeightClass.objects.get( + weight_class_code='LDV') ) for each_supplier in [initiating_supplier, recieving_supplier]: reduce_total = each_supplier == transfer.debit_from @@ -259,6 +260,3 @@ def validate_transfer(transfer): credit_transaction=added_transaction, organization_id=each_supplier.id ) - - - diff --git a/backend/api/services/sales_spreadsheet.py b/backend/api/services/sales_spreadsheet.py index 98323cf48..9c96ebcd1 100644 --- a/backend/api/services/sales_spreadsheet.py +++ b/backend/api/services/sales_spreadsheet.py @@ -8,6 +8,7 @@ from dateutil.parser import parse from django.core.exceptions import ValidationError +from django.db.models import Subquery import xlrd import xlwt @@ -430,12 +431,13 @@ def create_errors_spreadsheet(submission_id, organization_id, stream): worksheet.write(row, 4, 'Sales Date', style=BOLD) worksheet.write(row, 5, 'Error', style=BOLD) - record_of_sales_vin = RecordOfSale.objects.filter( + record_of_sales_vin = Subquery(RecordOfSale.objects.filter( submission_id=submission_id - ).values('vin') + ).values_list('vin', flat=True)) submission_content = SalesSubmissionContent.objects.filter( - submission_id=submission_id + submission_id=submission_id, + submission__organization_id=organization.id ).exclude( xls_vin__in=record_of_sales_vin ) diff --git a/backend/api/tests/test_credit_transfers.py b/backend/api/tests/test_credit_transfers.py index 16dd1cd35..e33583c41 100644 --- a/backend/api/tests/test_credit_transfers.py +++ b/backend/api/tests/test_credit_transfers.py @@ -1,8 +1,17 @@ from django.utils.datetime_safe import datetime -from rest_framework.serializers import ValidationError +from django.core.exceptions import ValidationError from .base_test_case import BaseTestCase from ..models.credit_transfer import CreditTransfer +from ..models.credit_transaction import CreditTransaction +from ..models.credit_transfer_content import CreditTransferContent +from ..models.credit_class import CreditClass +from ..models.model_year import ModelYear +from ..models.weight_class import WeightClass +from ..models.credit_transaction_type import CreditTransactionType +from ..services.credit_transaction import validate_transfer +from ..models.account_balance import AccountBalance + class TestTransfers(BaseTestCase): def setUp(self): @@ -31,6 +40,8 @@ def setUp(self): debit_from=org2, ) + + def test_list_transfer(self): response = self.clients['RTAN_BCEID'].get("/api/credit-transfers") self.assertEqual(response.status_code, 200) @@ -48,3 +59,88 @@ def test_list_transfer_gov(self): self.assertEqual(response.status_code, 200) result = response.data self.assertEqual(len(result), 1) + + # test that if the supplier does not have enough credits of specific + # type for specified year the transfer will fail and the database + # won't change + def test_transfer_fail(self): + transfer_not_enough = CreditTransfer.objects.create( + debit_from=self.users['RTAN_BCEID'].organization, + status='RECOMMEND_APPROVAL', + credit_to=self.users['EMHILLIE_BCEID'].organization, + ) + transfer_content = CreditTransferContent.objects.create( + model_year=ModelYear.objects.get(name='2020'), + credit_class=CreditClass.objects.get(credit_class="A"), + weight_class=WeightClass.objects.get(weight_class_code='LDV'), + credit_value=10, + dollar_value=10, + credit_transfer=transfer_not_enough, + ) + + # try changing from status RECOMMENDED to ISSUED, this should fail + # ie it should throw a Validation Error + self.assertRaises( + ValidationError, validate_transfer, transfer_not_enough) + + + # test that if the supplier does have enough of credit type/year the + # transfer will work, a record will be added for each row in transfer + # content (or unique credit type/year/weight) in the transaction table, + # and a new row for each credit type for each organization in the + # account balance table. + + def test_transfer_pass(self): + transaction = CreditTransaction.objects.create( + credit_to=self.users['EMHILLIE_BCEID'].organization, + model_year=ModelYear.objects.get(name='2020'), + credit_class=CreditClass.objects.get(credit_class="A"), + weight_class=WeightClass.objects.get(weight_class_code='LDV'), + credit_value=4, + number_of_credits=100, + total_value=400, + transaction_type=CreditTransactionType.objects.get( + transaction_type="Validation") + ) + transfer_enough = CreditTransfer.objects.create( + credit_to=self.users['RTAN_BCEID'].organization, + status='RECOMMEND_APPROVAL', + debit_from=self.users['EMHILLIE_BCEID'].organization, + update_user=self.users['EMHILLIE_BCEID'], + ) + + transfer_content = CreditTransferContent.objects.create( + model_year=ModelYear.objects.get(name='2020'), + credit_class=CreditClass.objects.get(credit_class="A"), + weight_class=WeightClass.objects.get(weight_class_code='LDV'), + credit_value=10, + dollar_value=10, + credit_transfer=transfer_enough, + ) + + validate_transfer(transfer_enough) + + seller_balance = AccountBalance.objects.filter( + organization_id=self.users['EMHILLIE_BCEID'].organization.id, + expiration_date=None, + credit_class=CreditClass.objects.get(credit_class="A") + ).first() + + buyer_balance = AccountBalance.objects.filter( + organization_id=self.users['RTAN_BCEID'].organization.id, + expiration_date=None, + credit_class=CreditClass.objects.get(credit_class="A") + ).first() + + self.assertEqual(seller_balance.balance, -10) + self.assertEqual(buyer_balance.balance, 10) + + credit_transaction_record = CreditTransaction.objects.filter( + credit_to=self.users['RTAN_BCEID'].organization, + debit_from=self.users['EMHILLIE_BCEID'].organization, + credit_class=CreditClass.objects.get(credit_class="A"), + ).order_by('-id').first() + self.assertEqual(credit_transaction_record.id, seller_balance.credit_transaction_id) + + + diff --git a/backend/api/tests/test_vehicles.py b/backend/api/tests/test_vehicles.py index 701cdb561..dcb9b8459 100644 --- a/backend/api/tests/test_vehicles.py +++ b/backend/api/tests/test_vehicles.py @@ -2,14 +2,35 @@ from .base_test_case import BaseTestCase from ..models.vehicle import Vehicle +from ..models.user_role import UserRole +from ..models.role import Role class TestVehicles(BaseTestCase): def test_get_vehicles(self): + UserRole.objects.create( + user_profile_id=self.users['RTAN_BCEID'].id, + role=Role.objects.get( + role_code='ZEVA User', + ) + ) + response = self.clients['RTAN_BCEID'].get("/api/vehicles") self.assertEqual(response.status_code, 200) def test_update_vehicle_state(self): + UserRole.objects.create( + user_profile_id=self.users['RTAN_BCEID'].id, + role=Role.objects.get( + role_code='Signing Authority', + ) + ) + UserRole.objects.create( + user_profile_id=self.users['RTAN_BCEID'].id, + role=Role.objects.get( + role_code='ZEVA User', + ) + ) org1 = self.users['RTAN_BCEID'].organization vehicle = Vehicle.objects.filter(organization=org1).first() diff --git a/backend/api/viewsets/credit_request.py b/backend/api/viewsets/credit_request.py index 078b55344..5c918e655 100644 --- a/backend/api/viewsets/credit_request.py +++ b/backend/api/viewsets/credit_request.py @@ -1,17 +1,27 @@ import json from datetime import datetime +from django.core.paginator import Paginator from django.core.exceptions import ValidationError from django.core.serializers.json import DjangoJSONEncoder -from django.http import HttpResponse +from django.db.models import Subquery, Count +from django.db.models import Q +from django.http import HttpResponse, HttpResponseForbidden + from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from api.models.icbc_registration_data import IcbcRegistrationData +from api.models.record_of_sale import RecordOfSale from api.models.sales_submission import SalesSubmission +from api.models.sales_submission_content import SalesSubmissionContent from api.models.sales_submission_statuses import SalesSubmissionStatuses from api.serializers.sales_submission import SalesSubmissionSerializer, \ SalesSubmissionListSerializer, SalesSubmissionSaveSerializer +from api.serializers.sales_submission_content import \ + SalesSubmissionContentSerializer from api.services.credit_transaction import award_credits from api.services.sales_spreadsheet import create_sales_spreadsheet, \ ingest_sales_spreadsheet, validate_spreadsheet, \ @@ -50,6 +60,7 @@ def get_queryset(self): 'retrieve': SalesSubmissionSerializer, 'partial_update': SalesSubmissionSaveSerializer, 'update': SalesSubmissionSaveSerializer, + 'content': SalesSubmissionContentSerializer, } def get_serializer_class(self): @@ -149,3 +160,113 @@ def download_errors(self, request, pk): ) ) return response + + @action(detail=True) + def content(self, request, pk): + filters = request.GET.get('filters') + page_size = request.GET.get('page_size', 20) + page = request.GET.get('page', 1) + sort_by = request.GET.get('sorted') + + try: + page = int(page) + except: + page = 1 + + if page < 1: + page = 1 + + # only government should be able to view the contents for icbc + # verification + if not request.user.is_government: + return HttpResponseForbidden() + + submission_content = SalesSubmissionContent.objects.filter( + submission_id=pk + ) + + if filters: + submission_filters = json.loads(filters) + + if 'xls_make' in submission_filters: + submission_content = submission_content.filter( + xls_make__icontains=submission_filters['xls_make'] + ) + + if 'xls_model' in submission_filters: + submission_content = submission_content.filter( + xls_model__icontains=submission_filters['xls_model'] + ) + + if 'xls_model_year' in submission_filters: + submission_content = submission_content.filter( + xls_model_year__icontains=submission_filters['xls_model_year'] + ) + + if 'xls_vin' in submission_filters: + submission_content = submission_content.filter( + xls_vin__icontains=submission_filters['xls_vin'] + ) + + if 'warning' in submission_filters: + duplicate_vins = Subquery(submission_content.annotate( + vin_count=Count('xls_vin') + ).filter(vin_count__gt=1).values_list('xls_vin', flat=True)) + + awarded_vins = Subquery(RecordOfSale.objects.exclude( + submission_id=pk + ).values_list('vin', flat=True)) + + submission_content = submission_content.filter( + Q(xls_vin__in=duplicate_vins) | + Q(xls_vin__in=awarded_vins) | + ~Q(xls_vin__in=Subquery( + IcbcRegistrationData.objects.values('vin') + )) | + Q(xls_sale_date__lte="43102.0") + ) + + if sort_by: + order_by = [] + sort_by_list = sort_by.split(',') + for sort in sort_by_list: + if sort in [ + 'xls_make', 'xls_model', 'xls_model_year', + 'xls_sale_date', 'xls_vin', + '-xls_make', '-xls_model', '-xls_model_year', + '-xls_sale_date', '-xls_vin', + ]: + order_by.append(sort) + + if order_by: + submission_content = submission_content.order_by(*order_by) + + submission_content_paginator = Paginator(submission_content, page_size) + + paginated = submission_content_paginator.page(page) + + serializer = SalesSubmissionContentSerializer( + paginated, many=True, read_only=True, context={'request': request} + ) + + return Response({ + 'content': serializer.data, + 'pages': submission_content_paginator.num_pages + }) + + @action(detail=True) + def unselected(self, request, pk): + if not request.user.is_government: + return HttpResponseForbidden() + + selected_vins = Subquery(RecordOfSale.objects.filter( + submission_id=pk + ).values_list('vin', flat=True)) + + unselected_vins = SalesSubmissionContent.objects.filter( + submission_id=pk + ).exclude( + xls_vin__in=selected_vins + ).values_list('id', flat=True) + + return Response(list(unselected_vins)) diff --git a/frontend/package.json b/frontend/package.json index cd23f8b1c..7a226ab4d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "zeva-frontend", - "version": "1.15.0", + "version": "1.15.0-1", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.13.0", diff --git a/frontend/src/app/routes/CreditRequests.js b/frontend/src/app/routes/CreditRequests.js index 4a75501e8..df1deb5bc 100644 --- a/frontend/src/app/routes/CreditRequests.js +++ b/frontend/src/app/routes/CreditRequests.js @@ -2,12 +2,14 @@ const API_BASE_PATH = '/credit-requests'; const CREDIT_REQUESTS = { CONFIRM: `${API_BASE_PATH}/:id/confirm`, + CONTENT: `${API_BASE_PATH}/:id/content`, DETAILS: `${API_BASE_PATH}/:id`, DOWNLOAD_ERRORS: `${API_BASE_PATH}/:id/download_errors`, EDIT: `${API_BASE_PATH}/:id/edit`, LIST: API_BASE_PATH, NEW: `${API_BASE_PATH}/new`, TEMPLATE: `${API_BASE_PATH}/template`, + UNSELECTED: `${API_BASE_PATH}/:id/unselected`, UPLOAD: `${API_BASE_PATH}/upload`, VALIDATE: `${API_BASE_PATH}/:id/validate`, VALIDATED: `${API_BASE_PATH}/:id/validated`, diff --git a/frontend/src/credits/CreditRequestVINListContainer.js b/frontend/src/credits/CreditRequestVINListContainer.js index f85b86be3..98ddf81ea 100644 --- a/frontend/src/credits/CreditRequestVINListContainer.js +++ b/frontend/src/credits/CreditRequestVINListContainer.js @@ -16,34 +16,28 @@ const CreditRequestVINListContainer = (props) => { const { match, user } = props; const { id } = match.params; + const [content, setContent] = useState([]); const [submission, setSubmission] = useState([]); const [loading, setLoading] = useState(true); - const [validatedList, setValidatedList] = useState([]); + const [invalidatedList, setInvalidatedList] = useState([]); const refreshDetails = () => { - axios.get(ROUTES_CREDIT_REQUESTS.DETAILS.replace(':id', id)).then((response) => { - const { data } = response; - setSubmission(data); - - const validatedRecords = data.content.filter( - (record) => { - if (record.warnings && record.warnings.some((warning) => [ - 'DUPLICATE_VIN', 'INVALID_MODEL', 'VIN_ALREADY_AWARDED', - ].indexOf(warning) >= 0)) { - return false; - } - - if (data.validationStatus === 'CHECKED') { - return record.recordOfSale; - } - - return record.icbcVerification; - }, - ).map((record) => record.id); - - setValidatedList(validatedRecords); + axios.all([ + axios.get(ROUTES_CREDIT_REQUESTS.DETAILS.replace(':id', id)), + axios.get(ROUTES_CREDIT_REQUESTS.CONTENT.replace(':id', id)), + axios.get(ROUTES_CREDIT_REQUESTS.UNSELECTED.replace(':id', id)), + ]).then(axios.spread((submissionResponse, contentResponse, unselectedResponse) => { + const { data: submissionData } = submissionResponse; + setSubmission(submissionData); + + const { data } = contentResponse; + setContent(data.content); + + const { data: unselected } = unselectedResponse; + setInvalidatedList(unselected); + setLoading(false); - }); + })); }; useEffect(() => { @@ -54,15 +48,17 @@ const CreditRequestVINListContainer = (props) => { const { value: submissionId, checked } = event.target; const newId = Number(submissionId); if (!checked) { - setValidatedList(validatedList.filter((item) => Number(item) !== Number(submissionId))); + setInvalidatedList(() => [...invalidatedList, newId]); } else { - setValidatedList(() => [...validatedList, newId]); + setInvalidatedList(invalidatedList.filter((item) => Number(item) !== Number(submissionId))); } }; const handleSubmit = () => { + setLoading(true); + axios.patch(ROUTES_CREDIT_REQUESTS.DETAILS.replace(':id', id), { - records: validatedList, + invalidated: invalidatedList, validationStatus: 'CHECKED', }).then(() => { const url = ROUTES_CREDIT_REQUESTS.VALIDATED.replace(/:id/g, submission.id); @@ -77,12 +73,14 @@ const CreditRequestVINListContainer = (props) => { return ( ); }; diff --git a/frontend/src/credits/UploadCreditRequestContainer.js b/frontend/src/credits/UploadCreditRequestContainer.js index 2226bfa67..227e16098 100644 --- a/frontend/src/credits/UploadCreditRequestContainer.js +++ b/frontend/src/credits/UploadCreditRequestContainer.js @@ -8,6 +8,7 @@ import { useParams } from 'react-router-dom'; import CreditTransactionTabs from '../app/components/CreditTransactionTabs'; import history from '../app/History'; +import Loading from '../app/components/Loading'; import ROUTES_CREDIT_REQUESTS from '../app/routes/CreditRequests'; import CustomPropTypes from '../app/utilities/props'; import { upload } from '../app/utilities/upload'; @@ -17,10 +18,12 @@ const UploadCreditRequestsContainer = (props) => { const { user } = props; const [errorMessage, setErrorMessage] = useState(null); const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); const { id } = useParams(); const doUpload = () => { + setLoading(true); let data = {}; if (id) { @@ -40,11 +43,17 @@ const UploadCreditRequestsContainer = (props) => { } else { setErrorMessage('An error has occurred while uploading. Please try again later.'); } + + setLoading(false); }); }; useEffect(() => {}, []); + if (loading) { + return (); + } + return ([ , { const recordsAddress = submission.organization.organizationAddress.find((address) => address.addressType.addressType === 'Records'); const downloadErrors = (e) => { - const element = e.target; + const element = e.currentTarget; const original = element.innerHTML; - element.firstChild.textContent = ' Downloading...'; + + element.innerText = 'Downloading...'; + element.disabled = true; + return download(ROUTES_CREDIT_REQUESTS.DOWNLOAD_ERRORS.replace(':id', submission.id), {}).then(() => { element.innerHTML = original; + element.disabled = false; }); }; diff --git a/frontend/src/credits/components/CreditRequestListTable.js b/frontend/src/credits/components/CreditRequestListTable.js index 4329052a8..72c757edf 100644 --- a/frontend/src/credits/components/CreditRequestListTable.js +++ b/frontend/src/credits/components/CreditRequestListTable.js @@ -47,9 +47,9 @@ const CreditRequestListTable = (props) => { }, { accessor: (item) => (item.totalWarnings > 0 ? item.totalWarnings : '-'), className: 'text-right', - Header: 'Warnings', + Header: 'Not Eligible for Credits', id: 'warnings', - maxWidth: 150, + maxWidth: 250, }, { accessor: (item) => (item.totalCredits && item.totalCredits.a > 0 ? formatNumeric(item.totalCredits.a) : '-'), className: 'text-right', diff --git a/frontend/src/credits/components/CreditRequestVINListPage.js b/frontend/src/credits/components/CreditRequestVINListPage.js index ef44c590d..11220f3da 100644 --- a/frontend/src/credits/components/CreditRequestVINListPage.js +++ b/frontend/src/credits/components/CreditRequestVINListPage.js @@ -7,11 +7,13 @@ import VINListTable from './VINListTable'; const CreditRequestVINListPage = (props) => { const { + content, handleCheckboxClick, handleSubmit, + setContent, submission, user, - validatedList, + invalidatedList, } = props; const [filtered, setFiltered] = useState([]); @@ -90,13 +92,15 @@ const CreditRequestVINListPage = (props) => {
@@ -113,14 +117,15 @@ const CreditRequestVINListPage = (props) => { CreditRequestVINListPage.defaultProps = {}; CreditRequestVINListPage.propTypes = { + content: PropTypes.arrayOf(PropTypes.shape()).isRequired, handleCheckboxClick: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, - submission: PropTypes.shape().isRequired, - user: CustomPropTypes.user.isRequired, - validatedList: PropTypes.arrayOf(PropTypes.oneOfType([ + invalidatedList: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.number, PropTypes.string, ])).isRequired, + submission: PropTypes.shape().isRequired, + user: CustomPropTypes.user.isRequired, }; export default CreditRequestVINListPage; diff --git a/frontend/src/credits/components/CreditTransfersDetailsPage.js b/frontend/src/credits/components/CreditTransfersDetailsPage.js index 2959d0f70..43b6b4ed3 100644 --- a/frontend/src/credits/components/CreditTransfersDetailsPage.js +++ b/frontend/src/credits/components/CreditTransfersDetailsPage.js @@ -196,11 +196,11 @@ const CreditTransfersDetailsPage = (props) => {

Light Duty Vehicle Credit Transfer

{permissions.governmentDirector && submission.creditTransferComment - && ( -
- -
- )} + && ( +
+ +
+ )} {permissions.governmentAnalyst && negativeCredit && ( diff --git a/frontend/src/credits/components/SubmissionListTable.js b/frontend/src/credits/components/SubmissionListTable.js deleted file mode 100644 index 6c4fefe31..000000000 --- a/frontend/src/credits/components/SubmissionListTable.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Presentational component - */ -import PropTypes from 'prop-types'; -import React from 'react'; - -import CustomPropTypes from '../../app/utilities/props'; -import ReactTable from '../../app/components/ReactTable'; -import formatNumeric from '../../app/utilities/formatNumeric'; -import formatStatus from '../../app/utilities/formatStatus'; -import history from '../../app/History'; -import ROUTES_CREDITS from '../../app/routes/Credits'; - -const SubmissionListTable = (props) => { - const { - items, filtered, setFiltered, user, - } = props; - - const columns = [{ - accessor: 'id', - className: 'text-right', - Header: 'ID', - maxWidth: 75, - }, { - accessor: 'submissionDate', - className: 'text-center', - Header: 'Date', - }, { - accessor: 'updateUser.displayName', - className: 'text-left', - Header: 'Last User', - id: 'updateUser', - }, { - accessor: (item) => (item.organization ? item.organization.name : ''), - className: 'text-left', - Header: 'Supplier', - id: 'supplier', - show: user.isGovernment, - }, { - accessor: (item) => { - if (['DRAFT', 'SUBMITTED'].indexOf(item.validationStatus) >= 0) { - const totals = item.totals.vins + item.unselected; - return (totals > 0 ? totals : '-'); - } - - return (item.totals.vins > 0 ? item.totals.vins : '-'); - }, - className: 'text-right', - Header: 'Total Sales', - maxWidth: 150, - id: 'total-sales', - }, { - accessor: (item) => (item.totalWarnings > 0 ? item.totalWarnings : '-'), - className: 'text-right', - Header: 'Warnings', - id: 'warnings', - maxWidth: 150, - }, { - accessor: (item) => (item.totalACredits > 0 ? formatNumeric(item.totalACredits) : '-'), - className: 'text-right', - Header: 'A-Credits', - id: 'credits-a', - maxWidth: 150, - }, { - accessor: (item) => (item.totalBCredits > 0 ? formatNumeric(item.totalBCredits) : '-'), - className: 'text-right', - Header: 'B-Credits', - id: 'credits-b', - maxWidth: 150, - }, { - accessor: (item) => { - const { validationStatus } = item; - const status = formatStatus(validationStatus); - - if (status === 'checked') { - return 'validated'; - } - - if (status === 'validated') { - return 'issued'; - } - - if (status === 'recommend approval') { - return 'recommend issuance'; - } - - return status; - }, - className: 'text-center text-capitalize', - filterMethod: (filter, row) => { - const filterValues = filter.value.split(','); - - let returnValue = false; - - filterValues.forEach((filterValue) => { - const value = filterValue.toLowerCase().trim(); - - if (value !== '' && !returnValue) { - returnValue = row[filter.id].toLowerCase().includes(value); - } - }); - - return returnValue; - }, - Header: 'Status', - id: 'status', - maxWidth: 250, - }]; - - return ( - { - if (row && row.original) { - return { - onClick: () => { - const { id } = row.original; - - history.push(ROUTES_CREDITS.CREDIT_REQUEST_DETAILS.replace(/:id/g, id), filtered); - }, - className: 'clickable', - }; - } - - return {}; - }} - setFiltered={setFiltered} - /> - ); -}; - -SubmissionListTable.defaultProps = { - filtered: undefined, - setFiltered: undefined, -}; - -SubmissionListTable.propTypes = { - filtered: PropTypes.arrayOf(PropTypes.shape()), - items: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - setFiltered: PropTypes.func, - user: CustomPropTypes.user.isRequired, -}; - -export default SubmissionListTable; diff --git a/frontend/src/credits/components/VINListTable.js b/frontend/src/credits/components/VINListTable.js index bc8160ac4..b0c0b77fe 100644 --- a/frontend/src/credits/components/VINListTable.js +++ b/frontend/src/credits/components/VINListTable.js @@ -1,24 +1,31 @@ /* * Presentational component */ +import axios from 'axios'; import moment from 'moment-timezone'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState } from 'react'; +import ReactTable from 'react-table'; -import ReactTable from '../../app/components/ReactTable'; import CREDIT_ERROR_CODES from '../../app/constants/errorCodes'; +import ROUTES_CREDIT_REQUESTS from '../../app/routes/CreditRequests'; import CustomPropTypes from '../../app/utilities/props'; const VINListTable = (props) => { const { handleCheckboxClick, + id, items, user, - validatedList, + invalidatedList, filtered, + setContent, setFiltered, } = props; + const [loading, setLoading] = useState(false); + const [pages, setPages] = useState(-1); + const getErrorCodes = (item, fields = false) => { let errorCodes = ''; @@ -57,20 +64,21 @@ const VINListTable = (props) => { }, className: 'text-center', Header: 'MY', - id: 'model-year', + id: 'xls_model_year', }, { accessor: 'xlsMake', Header: 'Make', - id: 'make', + id: 'xls_make', }, { accessor: 'xlsModel', Header: 'Model', - id: 'model', + id: 'xls_model', }, { accessor: (row) => (moment(row.salesDate).format('YYYY-MM-DD') !== 'Invalid date' ? moment(row.salesDate).format('YYYY-MM-DD') : row.salesDate), className: 'text-center sales-date', + filterable: false, Header: 'Retail Sale', - id: 'sales-date', + id: 'xls_sale_date', }], }, { Header: '', @@ -80,7 +88,7 @@ const VINListTable = (props) => { className: 'vin', Header: 'VIN', headerClassName: 'vin', - id: 'vin', + id: 'xls_vin', minWidth: 150, }], }, { @@ -89,17 +97,23 @@ const VINListTable = (props) => { columns: [{ accessor: (item) => (item.icbcVerification ? item.icbcVerification.icbcVehicle.modelYear.name : '-'), className: 'icbc-model-year text-center', + filterable: false, + sortable: false, Header: 'MY', headerClassName: 'icbc-model-year', id: 'icbc-model-year', }, { accessor: (item) => (item.icbcVerification ? item.icbcVerification.icbcVehicle.make : '-'), className: 'icbc-make', + filterable: false, + sortable: false, Header: 'Make', id: 'icbc-make', }, { accessor: (item) => (item.icbcVerification ? item.icbcVerification.icbcVehicle.modelName : '-'), className: 'icbc-model', + filterable: false, + sortable: false, Header: 'Model', id: 'icbc-model', }], @@ -123,7 +137,7 @@ const VINListTable = (props) => { return ( Number(item) === Number(row.id)) >= 0 + invalidatedList.findIndex((item) => Number(item) === Number(row.id)) < 0 } onChange={(event) => { handleCheckboxClick(event); }} type="checkbox" @@ -132,6 +146,8 @@ const VINListTable = (props) => { ); }, className: 'text-center validated', + filterable: false, + sortable: false, Header: 'Validated', id: 'validated', show: user.isGovernment, @@ -143,11 +159,12 @@ const VINListTable = (props) => { columns={columns} data={items} filtered={filtered} + filterable onFilteredChange={(input) => { setFiltered(input); }} defaultSorted={[{ - id: 'warning', + id: 'xls_vin', desc: true, }]} getTrProps={(state, rowInfo) => { @@ -186,6 +203,45 @@ const VINListTable = (props) => { } return {}; }} + loading={loading} + manual + onFetchData={(state) => { + setLoading(true); + + const filters = {}; + + state.filtered.forEach((each) => { + filters[each.id] = each.value; + }); + + const sorted = []; + + state.sorted.forEach((each) => { + let value = each.id; + + if (each.desc) { + value = `-${value}`; + } + + sorted.push(value); + }); + + axios.get(ROUTES_CREDIT_REQUESTS.CONTENT.replace(':id', id), { + params: { + filters, + page: state.page + 1, // page from front-end is zero index, but in the back-end we need the actual page number + page_size: state.pageSize, + sorted: sorted.join(','), + }, + }).then((response) => { + const { content, pages: numPages } = response.data; + + setContent(content); + setPages(numPages); + setLoading(false); + }); + }} + pages={pages} /> ); }; @@ -199,7 +255,7 @@ VINListTable.propTypes = { handleCheckboxClick: PropTypes.func.isRequired, items: PropTypes.arrayOf(PropTypes.shape({})).isRequired, user: CustomPropTypes.user.isRequired, - validatedList: PropTypes.arrayOf(PropTypes.oneOfType([ + invalidatedList: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.number, PropTypes.string, ])).isRequired, diff --git a/openshift-v3/templates/backend/README.md b/openshift-v3/templates/backend/README.md index e7e441915..303b1798d 100644 --- a/openshift-v3/templates/backend/README.md +++ b/openshift-v3/templates/backend/README.md @@ -15,6 +15,9 @@ 2. Create template secret template.django-secret * oc process -f django-secret-template.yaml | oc create -f - -n [project namespace] +3. Create email service secret for each environment + * oc process -f email-service-secret.yaml EMAIL_SERVICE_CLIENT_ID= EMAIL_SERVICE_CLIENT_SECRET= CHES_AUTH_URL= CHES_EMAIL_URL= | oc create -f - -n [env namespace] + #### After pipeline completes 1. After pipeline completes, create autoscaler for backend diff --git a/openshift-v3/templates/backend/backend-dc.yaml b/openshift-v3/templates/backend/backend-dc.yaml index 2f8909ef6..b6c6b88fe 100644 --- a/openshift-v3/templates/backend/backend-dc.yaml +++ b/openshift-v3/templates/backend/backend-dc.yaml @@ -318,16 +318,6 @@ objects: configMapKeyRef: name: ${NAME}-config${SUFFIX} key: rabbitmq_port - - name: SMTP_SERVER_HOST - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_host - - name: SMTP_SERVER_PORT - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_port - name: EMAIL_SENDING_ENABLED valueFrom: configMapKeyRef: @@ -362,6 +352,26 @@ objects: configMapKeyRef: name: ${NAME}-config${SUFFIX} key: minio_endpoint + - name: EMAIL_SERVICE_CLIENT_ID + valueFrom: + secretKeyRef: + name: email-service + key: EMAIL_SERVICE_CLIENT_ID + - name: EMAIL_SERVICE_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: email-service + key: EMAIL_SERVICE_CLIENT_SECRET + - name: CHES_AUTH_URL + valueFrom: + secretKeyRef: + name: email-service + key: CHES_AUTH_URL + - name: CHES_EMAIL_URL + valueFrom: + secretKeyRef: + name: email-service + key: CHES_EMAIL_URL livenessProbe: failureThreshold: 30 tcpSocket: diff --git a/openshift-v3/templates/backend/email-service-secret.yaml b/openshift-v3/templates/backend/email-service-secret.yaml new file mode 100644 index 000000000..2de0bf6c9 --- /dev/null +++ b/openshift-v3/templates/backend/email-service-secret.yaml @@ -0,0 +1,26 @@ +apiVersion: template.openshift.io/v1 +kind: Template +parameters: +- name: EMAIL_SERVICE_CLIENT_ID + description: the client id for Zeva project + required: true +- name: EMAIL_SERVICE_CLIENT_SECRET + description: the secrete for Zeva project + required: true +- name: CHES_AUTH_URL + description: the authentication url to retrieve token + required: true +- name: CHES_EMAIL_URL + description: the email service url + required: true +objects: +- apiVersion: v1 + kind: Secret + metadata: + annotations: null + name: email-service + stringData: + EMAIL_SERVICE_CLIENT_ID: ${EMAIL_SERVICE_CLIENT_ID} + EMAIL_SERVICE_CLIENT_SECRET: ${EMAIL_SERVICE_CLIENT_SECRET} + CHES_AUTH_URL: ${CHES_AUTH_URL} + CHES_EMAIL_URL: ${CHES_EMAIL_URL} \ No newline at end of file diff --git a/openshift-v3/templates/config/configmap.yaml b/openshift-v3/templates/config/configmap.yaml index 8f1a63308..28d778349 100644 --- a/openshift-v3/templates/config/configmap.yaml +++ b/openshift-v3/templates/config/configmap.yaml @@ -44,7 +44,11 @@ parameters: - name: OCP_NAME displayName: Openshift Cluster Name description: Openshift Cluster Name - required: true + required: true + - name: LOGOUT_HOST + displayName: the logout host + description: the logout host + required: true objects: - apiVersion: v1 kind: ConfigMap @@ -65,7 +69,7 @@ objects: keycloak_certs_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs keycloak_client_id: zeva keycloak_issuer: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM} - keycloak_logout_redirect_url: https://logontest.gov.bc.ca/clp-cgi/logoff.cgi?returl=https%3A%2F%2F${HOST_NAME}%2F + keycloak_logout_redirect_url: https://${LOGOUT_HOST}/clp-cgi/logoff.cgi?returl=https://${HOST_NAME}&retnow=1 keycloak_realm_name: ${KEYCLOAK_REALM} keycloak_realm_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM} keycloak_realm: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM} @@ -80,5 +84,3 @@ objects: rabbitmq_host: ${NAME}${SUFFIX}-rabbitmq-cluster.tbiwaq-${ENV_NAME}.svc.cluster.local rabbitmq_port: '5672' rabbitmq_vhost: /zeva - smtp_server_host: apps.smtp.gov.bc.ca - smtp_server_port: '25' diff --git a/openshift-v3/templates/frontend/frontend-dc.yaml b/openshift-v3/templates/frontend/frontend-dc.yaml index 45bb0c684..6afe51494 100644 --- a/openshift-v3/templates/frontend/frontend-dc.yaml +++ b/openshift-v3/templates/frontend/frontend-dc.yaml @@ -252,7 +252,7 @@ objects: terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - - mountPath: /app/src/app/config + - mountPath: /opt/app-root/src/src/app/config name: ${NAME}-frontend-config${SUFFIX} dnsPolicy: ClusterFirst restartPolicy: Always diff --git a/openshift-v3/templates/nagios/nagios-dc.yaml b/openshift-v3/templates/nagios/nagios-dc.yaml index a80e561fb..327991f4b 100644 --- a/openshift-v3/templates/nagios/nagios-dc.yaml +++ b/openshift-v3/templates/nagios/nagios-dc.yaml @@ -203,8 +203,6 @@ objects: configMapKeyRef: name: ${NAME}-config-${ENV_NAME} key: keycloak_realm - - name: SMTP_SERVER_HOST - value: apps.smtp.gov.bc.ca - name: DATABASE_SERVICE_NAME valueFrom: configMapKeyRef: diff --git a/openshift-v3/templates/patroni/README.md b/openshift-v3/templates/patroni/README.md index d7c3c72e3..28df32a91 100644 --- a/openshift-v3/templates/patroni/README.md +++ b/openshift-v3/templates/patroni/README.md @@ -34,7 +34,7 @@ login to patroni-backup pod and run backup.sh -1 5. Recover the backup to paroni database on Openshift v4 login patroini-backup pod on Openshift v4, run the following command -./backup.sh -r patroni-master-prod/zeva -f /backups/fromv3/postgresql-zeva_2020-08-28_19-06-28.sql.gz +./backup.sh -r patroni-master-prod:5432/zeva -f /backups/fromv3 //yes, folder name only, it will pickup the file and ask confirmation 6. Verify the database on Openshift v3 and v4 to make sure they are same diff --git a/openshift-v3/templates/unittest/backend-dc-unittest.yaml b/openshift-v3/templates/unittest/backend-dc-unittest.yaml index 27f0b0e39..2bbeb15ce 100644 --- a/openshift-v3/templates/unittest/backend-dc-unittest.yaml +++ b/openshift-v3/templates/unittest/backend-dc-unittest.yaml @@ -230,16 +230,6 @@ objects: configMapKeyRef: name: ${NAME}-config${SUFFIX} key: rabbitmq_port - - name: SMTP_SERVER_HOST - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_host - - name: SMTP_SERVER_PORT - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_port - name: EMAIL_SENDING_ENABLED valueFrom: configMapKeyRef: diff --git a/openshift/templates/backend/README.md b/openshift/templates/backend/README.md index f6dc0d0a3..3ffc45c15 100644 --- a/openshift/templates/backend/README.md +++ b/openshift/templates/backend/README.md @@ -15,6 +15,9 @@ 2. Create template secret template.django-secret * oc process -f django-secret-template.yaml | oc create -f - -n [project namespace] +3. Create email service secret for each environment + * oc process -f email-service-secret.yaml EMAIL_SERVICE_CLIENT_ID= EMAIL_SERVICE_CLIENT_SECRET= CHES_AUTH_URL= CHES_EMAIL_URL= | oc create -f - -n [env namespace] + #### After pipeline completes 1. After pipeline completes, create autoscaler for backend diff --git a/openshift/templates/backend/backend-dc.yaml b/openshift/templates/backend/backend-dc.yaml index 957167eb6..b32fed73c 100644 --- a/openshift/templates/backend/backend-dc.yaml +++ b/openshift/templates/backend/backend-dc.yaml @@ -314,16 +314,6 @@ objects: configMapKeyRef: name: ${NAME}-config${SUFFIX} key: rabbitmq_port - - name: SMTP_SERVER_HOST - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_host - - name: SMTP_SERVER_PORT - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_port - name: EMAIL_SENDING_ENABLED valueFrom: configMapKeyRef: @@ -358,6 +348,26 @@ objects: configMapKeyRef: name: ${NAME}-config${SUFFIX} key: minio_endpoint + - name: EMAIL_SERVICE_CLIENT_ID + valueFrom: + secretKeyRef: + name: email-service + key: EMAIL_SERVICE_CLIENT_ID + - name: EMAIL_SERVICE_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: email-service + key: EMAIL_SERVICE_CLIENT_SECRET + - name: CHES_AUTH_URL + valueFrom: + secretKeyRef: + name: email-service + key: CHES_AUTH_URL + - name: CHES_EMAIL_URL + valueFrom: + secretKeyRef: + name: email-service + key: CHES_EMAIL_URL livenessProbe: failureThreshold: 30 tcpSocket: diff --git a/openshift/templates/backend/email-service-secret.yaml b/openshift/templates/backend/email-service-secret.yaml new file mode 100644 index 000000000..2de0bf6c9 --- /dev/null +++ b/openshift/templates/backend/email-service-secret.yaml @@ -0,0 +1,26 @@ +apiVersion: template.openshift.io/v1 +kind: Template +parameters: +- name: EMAIL_SERVICE_CLIENT_ID + description: the client id for Zeva project + required: true +- name: EMAIL_SERVICE_CLIENT_SECRET + description: the secrete for Zeva project + required: true +- name: CHES_AUTH_URL + description: the authentication url to retrieve token + required: true +- name: CHES_EMAIL_URL + description: the email service url + required: true +objects: +- apiVersion: v1 + kind: Secret + metadata: + annotations: null + name: email-service + stringData: + EMAIL_SERVICE_CLIENT_ID: ${EMAIL_SERVICE_CLIENT_ID} + EMAIL_SERVICE_CLIENT_SECRET: ${EMAIL_SERVICE_CLIENT_SECRET} + CHES_AUTH_URL: ${CHES_AUTH_URL} + CHES_EMAIL_URL: ${CHES_EMAIL_URL} \ No newline at end of file diff --git a/openshift/templates/config/configmap.yaml b/openshift/templates/config/configmap.yaml index 748b8c7ba..09eacfa8b 100644 --- a/openshift/templates/config/configmap.yaml +++ b/openshift/templates/config/configmap.yaml @@ -69,7 +69,7 @@ objects: keycloak_certs_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs keycloak_client_id: zeva keycloak_issuer: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM} - keycloak_logout_redirect_url: https://${LOGOUT_HOST_NAME}/clp-cgi/logoff.cgi?returl=https%3A%2F%2F${HOST_NAME}%2F + keycloak_logout_redirect_url: https://${LOGOUT_HOST_NAME}/clp-cgi/logoff.cgi?returl=https://${HOST_NAME}&retnow=1 keycloak_realm_name: ${KEYCLOAK_REALM} keycloak_realm_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM} keycloak_realm: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM} @@ -84,5 +84,3 @@ objects: rabbitmq_host: ${NAME}${SUFFIX}-rabbitmq-cluster.e52f12-${ENV_NAME}.svc.cluster.local rabbitmq_port: '5672' rabbitmq_vhost: /zeva - smtp_server_host: apps.smtp.gov.bc.ca - smtp_server_port: '25' diff --git a/openshift/templates/frontend/frontend-dc.yaml b/openshift/templates/frontend/frontend-dc.yaml index 02121ea11..6c13ccdd6 100644 --- a/openshift/templates/frontend/frontend-dc.yaml +++ b/openshift/templates/frontend/frontend-dc.yaml @@ -248,7 +248,7 @@ objects: terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - - mountPath: /app/src/app/config + - mountPath: /opt/app-root/src/src/app/config name: ${NAME}-frontend-config${SUFFIX} dnsPolicy: ClusterFirst restartPolicy: Always diff --git a/openshift/templates/nagios/nagios-dc.yaml b/openshift/templates/nagios/nagios-dc.yaml index 49af25b8f..3906b432a 100644 --- a/openshift/templates/nagios/nagios-dc.yaml +++ b/openshift/templates/nagios/nagios-dc.yaml @@ -203,8 +203,6 @@ objects: configMapKeyRef: name: ${NAME}-config-${ENV_NAME} key: keycloak_realm - - name: SMTP_SERVER_HOST - value: apps.smtp.gov.bc.ca - name: DATABASE_SERVICE_NAME valueFrom: configMapKeyRef: diff --git a/openshift/templates/nagios/nagios3/commands/check_email_connection.sh b/openshift/templates/nagios/nagios3/commands/check_email_connection.sh index e49e5ebd4..5a07ee1b9 100755 --- a/openshift/templates/nagios/nagios3/commands/check_email_connection.sh +++ b/openshift/templates/nagios/nagios3/commands/check_email_connection.sh @@ -1,10 +1,13 @@ #!/bin/bash -emailConnectionTest=$(python3 /etc/nagios3/commands/check_email_connection.py) -echo $emailConnectionTest -if [[ $emailConnectionTest == OK* ]]; -then - exit 0 -else - exit 2 -fi +exit 0 + +# comment out email connection checking as zeva switches to CHES +#emailConnectionTest=$(python3 /etc/nagios3/commands/check_email_connection.py) +#echo $emailConnectionTest +#if [[ $emailConnectionTest == OK* ]]; +#then +# exit 0 +#else +# exit 2 +#fi diff --git a/openshift/templates/patroni/README.md b/openshift/templates/patroni/README.md index aca0503eb..a30702849 100644 --- a/openshift/templates/patroni/README.md +++ b/openshift/templates/patroni/README.md @@ -37,7 +37,7 @@ login to patroni-backup pod and run backup.sh -1 5. Recover the backup to paroni database on Openshift v4 login patroini-backup pod on Openshift v4, run the following command -./backup.sh -r patroni-master-prod/zeva -f /backups/fromv3/postgresql-zeva_2020-08-28_19-06-28.sql.gz +./backup.sh -r patroni-master-prod/zeva -f /backups/fromv3 //yes, folder name only, it will pickup the file and ask confirmation 6. Verify the database on Openshift v3 and v4 to make sure they are same diff --git a/openshift/templates/unittest/backend-dc-unittest.yaml b/openshift/templates/unittest/backend-dc-unittest.yaml index a7fb74602..1bfaf7875 100644 --- a/openshift/templates/unittest/backend-dc-unittest.yaml +++ b/openshift/templates/unittest/backend-dc-unittest.yaml @@ -230,16 +230,6 @@ objects: configMapKeyRef: name: ${NAME}-config${SUFFIX} key: rabbitmq_port - - name: SMTP_SERVER_HOST - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_host - - name: SMTP_SERVER_PORT - valueFrom: - configMapKeyRef: - name: ${NAME}-config${SUFFIX} - key: smtp_server_port - name: EMAIL_SENDING_ENABLED valueFrom: configMapKeyRef: