From 9a09a687c2859117581e8090e14dd544bddf1239 Mon Sep 17 00:00:00 2001 From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:26:19 -0800 Subject: [PATCH] Tracking pull request to merge release-2.1.0 to master (#1966) * update to 2.1.0 * update to 2.1.0 * -changes text that is displayed after proposing to buy credit, so it now shows price per credit * fix: refresh token updates * fix: tfrs-556 - accessing assessment page * feat: tfrs-514 - inactive suppliers can't accept credit transfers * fix/feat - adds condition before appending edit and adding to permissions for comment * feat - adds supplier status to trade information page * ZELDA-549 - Schedule D in B persistence issue fix * ZELDA-558 - Notification page search box issue fix * fix: tfrs-559 - credit transactions in downloaded spreadsheet * chore: version bump on certifi * fix: responsive navbar and banner by adding a media query (and extra div) to navbar items and updating breakpoint for banner * add valut (#1991) * fix: tfrs-583 - compliance reporting assessment section * dockerfile changes * ZELDA-584- Submit more than 1 exclusion report fix * update pr number to tracking pr number 1966 * Quick fix - added loading component * ZELDA-594 update RLCF-017 link * update for db migration (#2004) * roll back minio (#2006) * ZELDA-600-fix Unable to create new fuel suppliers in TFRS * fix: removed duplicate jquery * fix: check for null records on scheduleDSheetIndex * update db upgrading doc (#2027) * feat: filters updated for pagination * ZELDA-595 Add Sort & filter to All Tables * fix: displayname filter fix Co-authored-by: emi-hi Co-authored-by: AlexZorkin Co-authored-by: tim738745 Co-authored-by: Emily <44536222+emi-hi@users.noreply.github.com> Co-authored-by: Alex Zorkin <47334977+AlexZorkin@users.noreply.github.com> Co-authored-by: vibhishan Co-authored-by: jig-patel <122304104+jig-patel@users.noreply.github.com> --- .github/workflows/build-release.yaml | 8 +- .gitignore | 1 + .pipeline/lib/build.js | 14 - .pipeline/lib/config.js | 6 +- backend/Dockerfile-django | 2 +- .../api/services/CreditTradeCommentActions.py | 23 +- backend/api/services/SpreadSheetBuilder.py | 14 +- backend/api/viewsets/ComplianceReport.py | 154 ++++++-- backend/api/viewsets/Document.py | 59 ++- backend/api/viewsets/Notification.py | 54 ++- backend/requirements.txt | 2 +- charts/tfrs-spilo/Chart.yaml | 4 +- charts/tfrs-spilo/Readme.md | 74 +++- charts/tfrs-spilo/values-test.yaml | 10 +- docker-compose.yml | 242 ++++++------ frontend/Dockerfile | 2 +- frontend/package.json | 3 +- frontend/src/actions/complianceReporting.js | 3 +- frontend/src/actions/documentUploads.js | 5 +- frontend/src/actions/keycloakActions.js | 12 +- frontend/src/actions/notificationActions.js | 5 +- frontend/src/actions/userActions.js | 1 - frontend/src/app/components/Modal.js | 1 - frontend/src/app/components/Navbar.js | 3 + .../ComplianceReportingContainer.js | 18 +- .../ScheduleAssessmentContainer.js | 15 + .../ScheduleBContainer.js | 14 +- .../components/ComplianceReportingPage.js | 18 +- .../components/ComplianceReportingTable.js | 80 ++-- .../components/ScheduleTabs.js | 30 +- .../services/ComplianceReportingService.js | 9 +- .../src/constants/routes/Organizations.js | 2 +- .../components/CreditTransferDetails.js | 6 +- .../components/CreditTransferFormButtons.js | 7 +- .../CreditTransferTextRepresentation.js | 370 ++++++++++-------- .../CreditTransferVisualRepresentation.js | 251 +++++++----- .../ExclusionReportEditContainer.js | 29 +- .../components/ExclusionReportButtons.js | 13 + .../notifications/NotificationsContainer.js | 16 +- .../components/NotificationsDetails.js | 5 +- .../components/NotificationsTable.js | 12 +- .../OrganizationEditContainer.js | 1 - frontend/src/reducers/keycloakReducer.js | 5 + frontend/src/reducers/rootSaga.js | 2 - .../SecureFileSubmissionContainer.js | 16 +- .../components/SecureFileSubmissionTable.js | 30 +- .../components/SecureFileSubmissionsPage.js | 5 +- frontend/src/store/authenticationState.js | 30 +- frontend/src/store/sessionTimeout.js | 18 +- frontend/src/store/store.js | 12 +- frontend/start.js | 21 +- frontend/styles/Navbar.scss | 31 +- frontend/styles/index.scss | 2 +- keycloak/Dockerfile-keycloak | 2 +- nginx/Dockerfile-nginx | 2 +- .../{ => notes}/tfrs-2.0.0-prod-migration.txt | 22 +- .../templates/backend/backend-dc.yaml | 18 +- openshift-v4/templates/celery/celery-dc.yaml | 18 +- .../templates/knp/knp-diagram-2.0.0.drawio | 2 +- .../knp/knp-env-pr-new-tfrs-spilo.yaml | 109 ++++++ openshift-v4/templates/knp/knp-env-pr.yaml | 2 +- .../scan-coordinator/scan-coordinator-dc.yaml | 13 +- openshift-v4/templates/spilo/s3-secret.yaml | 22 ++ openshift-v4/templates/vault/README.md | 83 ++++ .../templates/vault/create-manifests.sh | 9 + openshift-v4/templates/vault/deployment.yaml | 56 +++ .../templates/vault/vault-0ab226-dev.yaml | 56 +++ .../templates/vault/vault-0ab226-prod.yaml | 56 +++ .../templates/vault/vault-0ab226-test.yaml | 56 +++ .../templates/vault/vault-0ab226-tools.yaml | 56 +++ postgres/Dockerfile-postgres | 2 +- smtplogger/Dockerfile-smtplogger | 2 +- 72 files changed, 1646 insertions(+), 710 deletions(-) rename openshift-v4/{ => notes}/tfrs-2.0.0-prod-migration.txt (50%) create mode 100644 openshift-v4/templates/knp/knp-env-pr-new-tfrs-spilo.yaml create mode 100644 openshift-v4/templates/spilo/s3-secret.yaml create mode 100644 openshift-v4/templates/vault/README.md create mode 100755 openshift-v4/templates/vault/create-manifests.sh create mode 100644 openshift-v4/templates/vault/deployment.yaml create mode 100644 openshift-v4/templates/vault/vault-0ab226-dev.yaml create mode 100644 openshift-v4/templates/vault/vault-0ab226-prod.yaml create mode 100644 openshift-v4/templates/vault/vault-0ab226-test.yaml create mode 100644 openshift-v4/templates/vault/vault-0ab226-tools.yaml diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml index 611c6ae93..a5bb0c20b 100644 --- a/.github/workflows/build-release.yaml +++ b/.github/workflows/build-release.yaml @@ -1,18 +1,18 @@ ## For each release, the value of workflow name, branches and PR_NUMBER need to be adjusted accordingly -name: TFRS release-2.0.0 +name: TFRS release-2.1.0 on: push: - branches: [ release-2.0.0 ] + branches: [ release-2.1.0 ] workflow_dispatch: workflow_call: env: ## The pull request number of the Tracking pull request to merge the release branch to main ## Also remember to update the version in .pipeline/lib/config.js - PR_NUMBER: 1824 - RELEASE_NAME: release-2.0.0 + PR_NUMBER: 1966 + RELEASE_NAME: release-2.1.0 jobs: diff --git a/.gitignore b/.gitignore index 400ccc1dd..88ce92b61 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ dev/ node_modules/ .sql.gz +*.sql \ No newline at end of file diff --git a/.pipeline/lib/build.js b/.pipeline/lib/build.js index ec574192b..d40086530 100755 --- a/.pipeline/lib/build.js +++ b/.pipeline/lib/build.js @@ -25,20 +25,6 @@ module.exports = settings => { 'GIT_REF': oc.git.ref } })) - - /** to be deleted once 2.0.0 is on prod - //build frontend - objects = objects.concat(oc.processDeploymentTemplate(`${templatesLocalBaseUrl}/templates/frontend/frontend-angular-app-bc.yaml`, { - 'param':{ - 'NAME': phases[phase].name, - 'SUFFIX': phases[phase].suffix, - 'VERSION': phases[phase].tag, - 'GIT_URL': oc.git.http_url, - 'GIT_REF': oc.git.ref - } - })) - */ - objects = objects.concat(oc.processDeploymentTemplate(`${templatesLocalBaseUrl}/templates/frontend/frontend-bc-docker.yaml`, { 'param':{ diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index 6a8b63a76..4fc0da821 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 = '2.0.0' +const version = '2.1.0' const name = 'tfrs' const ocpName = 'apps.silver.devops' @@ -13,7 +13,7 @@ options.git.repository='tfrs' const phases = { build: { namespace:'0ab226-tools' , name: `${name}`, phase: 'build' , changeId:changeId, suffix: `-build-${changeId}` , instance: `${name}-build-${changeId}` , version:`${version}-${changeId}`, tag:`build-${version}-${changeId}`, - releaseBranch: 'release-2.0.0' + releaseBranch: 'release-2.1.0' }, dev: {namespace:'0ab226-dev' , name: `${name}`, phase: 'dev' , changeId:changeId, suffix: `-dev` , instance: `${name}-dev` , version:`${version}`, tag:`dev-${version}`, dbServiceName: 'tfrs-spilo', @@ -45,7 +45,7 @@ const phases = { schemaSpyAuditCpuRequest: '50m', schemaSpyAuditCpuLimit: '300m', schemaSpyAuditMemoryRequest: '256Mi', schemaSpyAuditMemoryLimit: '512Mi' }, test: {namespace:'0ab226-test' , name: `${name}`, phase: 'test' , changeId:changeId, suffix: `-test` , - instance: `${name}-test` , version:`${version}`, tag:`test-${version}`, dbServiceName: 'patroni-master-test', + instance: `${name}-test` , version:`${version}`, tag:`test-${version}`, dbServiceName: 'tfrs-spilo', frontendCpuRequest: '40m', frontendCpuLimit: '80m', frontendMemoryRequest: '60Mi', frontendMemoryLimit: '120Mi', frontendReplicas: 2, frontendKeycloakAuthority: 'https://test.loginproxy.gov.bc.ca/auth', frontendKeycloakClientId: 'tfrs-on-gold-4308', frontendKeycloakCallbackUrl: `https://tfrs-test.${ocpName}.gov.bc.ca`, frontendKeycloakLogoutUrl: `https://tfrs-test.${ocpName}.gov.bc.ca`, diff --git a/backend/Dockerfile-django b/backend/Dockerfile-django index 7ed6081ee..c81a89ba5 100644 --- a/backend/Dockerfile-django +++ b/backend/Dockerfile-django @@ -1,4 +1,4 @@ -FROM python:3.7-stretch +FROM --platform=linux/amd64 python:3.7-stretch ENV PYTHONUNBUFFERED 1 RUN mkdir /app WORKDIR /app diff --git a/backend/api/services/CreditTradeCommentActions.py b/backend/api/services/CreditTradeCommentActions.py index 454c8f606..4343d64a2 100644 --- a/backend/api/services/CreditTradeCommentActions.py +++ b/backend/api/services/CreditTradeCommentActions.py @@ -20,7 +20,9 @@ See the License for the specific language governing permissions and limitations under the License. """ -from api.models import CreditTrade, CreditTradeComment +from api.models import CreditTradeComment +from api.models.CreditTrade import CreditTrade + from api.models.CreditTradeHistory import CreditTradeHistory from api.permissions.CreditTradeComment import CreditTradeCommentPermissions @@ -35,22 +37,25 @@ class CreditTradeCommentActions(object): def available_comment_actions(request, trade: CreditTrade): available_actions = [] - if CreditTradeCommentPermissions.user_can_comment( - request.user, trade, False): - available_actions.append('ADD_COMMENT') + if not trade.is_rescinded and trade.status.status not in ['Declined', 'Cancelled', 'Refused', 'Approved']: - if CreditTradeCommentPermissions.user_can_comment( - request.user, trade, True): - available_actions.append('ADD_PRIVILEGED_COMMENT') + if CreditTradeCommentPermissions.user_can_comment( + request.user, trade, False): + available_actions.append('ADD_COMMENT') - return available_actions + if CreditTradeCommentPermissions.user_can_comment( + request.user, trade, True): + available_actions.append('ADD_PRIVILEGED_COMMENT') + + return available_actions @staticmethod def available_individual_comment_actions(request, comment: CreditTradeComment): available_actions = [] + trade = CreditTrade.objects.filter(id=comment.credit_trade_id).first() if CreditTradeCommentPermissions.user_can_edit_comment( - request.user, comment): + request.user, comment) and not trade.is_rescinded and trade.status.status not in ['Declined', 'Cancelled', 'Refused', 'Approved']: available_actions = ['EDIT_COMMENT'] return available_actions diff --git a/backend/api/services/SpreadSheetBuilder.py b/backend/api/services/SpreadSheetBuilder.py index 83e919130..254e455a5 100644 --- a/backend/api/services/SpreadSheetBuilder.py +++ b/backend/api/services/SpreadSheetBuilder.py @@ -138,9 +138,17 @@ def add_credit_transfers(self, credit_trades, user): credit_trade.fair_market_value_per_credit, value_format) - worksheet.write(row_index, 7, credit_trade.status.friendly_name) - worksheet.write(row_index, 8, credit_trade.trade_effective_date, - date_format) + if credit_trade.is_rescinded: + worksheet.write(row_index, 7, "Rescinded") + elif (not user.is_government_user) and (credit_trade.status.friendly_name == "Reviewed"): + worksheet.write(row_index, 7, "Signed") + else: + worksheet.write(row_index, 7, credit_trade.status.friendly_name) + + if credit_trade.trade_effective_date: + worksheet.write(row_index, 8, credit_trade.trade_effective_date, date_format) + elif credit_trade.type.the_type in ["Credit Reduction", "Credit Validation"] and credit_trade.update_timestamp: + worksheet.write(row_index, 8, credit_trade.update_timestamp.date(), date_format) comments = credit_trade.unprivileged_comments diff --git a/backend/api/viewsets/ComplianceReport.py b/backend/api/viewsets/ComplianceReport.py index 4a84e2543..34d9b0549 100644 --- a/backend/api/viewsets/ComplianceReport.py +++ b/backend/api/viewsets/ComplianceReport.py @@ -3,7 +3,7 @@ from django.db import transaction from django.db.models import Count -from django.http import JsonResponse, HttpResponse +from django.http import HttpResponse from rest_framework import viewsets, mixins, filters, status from rest_framework.decorators import action from rest_framework.permissions import AllowAny @@ -17,15 +17,15 @@ ComplianceReportTypeSerializer, ComplianceReportListSerializer, \ ComplianceReportCreateSerializer, ComplianceReportUpdateSerializer, \ ComplianceReportDeleteSerializer, ComplianceReportDetailSerializer, \ - ComplianceReportValidationSerializer, ComplianceReportSnapshotSerializer, \ - ComplianceReportDashboardListSerializer + ComplianceReportValidationSerializer, ComplianceReportDashboardListSerializer from api.serializers.ExclusionReport import \ ExclusionReportDetailSerializer, ExclusionReportUpdateSerializer, ExclusionReportValidationSerializer from api.services.ComplianceReportService import ComplianceReportService from api.services.ComplianceReportSpreadSheet import ComplianceReportSpreadsheet from auditable.views import AuditableMixin from api.paginations import BasicPagination -from django.db.models import Q +from django.db.models import Q, F, Value +from django.db.models.functions import Concat class ComplianceReportViewSet(AuditableMixin, mixins.CreateModelMixin, @@ -69,10 +69,35 @@ def get_queryset(self): request = self.request if self.action == 'list' or self.action == 'paginated': - qs = qs.annotate(Count('supplements')).filter(supplements__count=0)\ - .order_by('-compliance_period__effective_date') - + qs = qs.annotate(Count('supplements')).filter(supplements__count=0) if self.action == 'paginated': + sorts = request.data.get('sorts') + if sorts: + sortCondition = sorts[0].get('desc') + sortId = sorts[0].get('id') + key_maps = {'compliance-period':'compliance_period__description', 'organization':'organization__name', 'updateTimestamp':'compliance_period__effective_date'} + if sortId=='displayname': + if sortCondition: + qs = qs.annotate(display_name=Concat(F('type__the_type'), Value(' '), F('compliance_period__description'))).order_by('-display_name') + else: + qs = qs.annotate(display_name=Concat(F('type__the_type'), Value(' '), F('compliance_period__description'))).order_by('display_name') + elif sortId=='status': + if sortCondition: + qs =qs.order_by('-status__director_status__status', '-status__manager_status__status', '-status__analyst_status__status', '-status__fuel_supplier_status__status') + else: + qs = qs.order_by('status__director_status__status', 'status__manager_status__status', 'status__analyst_status__status', 'status__fuel_supplier_status__status') + elif sortId == 'supplemental-status': + # todo; + pass + elif sortId == 'current-status': + # todo; + pass + else: + sortType = "-" if sortCondition else "" + sortString = f"{sortType}{key_maps[sortId]}" + qs = qs.order_by(sortString) + else: + qs=qs.order_by('-compliance_period__effective_date') filters = request.data.get('filters') if filters: for filter in filters: @@ -81,29 +106,38 @@ def get_queryset(self): if id and value: if id == 'compliance-period': qs = qs.filter( - compliance_period__description__exact=value) + compliance_period__description__icontains=value) elif id == 'organization': qs = qs.filter( organization__name__icontains=value) elif id == 'displayname': - qs = qs.filter(Q(nickname__isnull=True) | Q( - nickname__icontains=value)) - # possible todo: deal with case where generated nicknames are used + qs = self.filter_displayname(qs, value.lower()) elif id == 'status': qs = self.filter_compliance_status( qs, value.lower()) elif id == 'supplemental-status': - # todo; same as the todo above, along with the fact that we'll have to somehow define (annotate) - # a deepest_supplemental_report field (via some sort of aggregation) which we can then filter on + qs = self.filter_supplemental_status( + qs, value.lower()) pass elif id == 'current-status': - # todo; same as the todo above + qs = self.filter_current_status( + qs, value.lower()) pass elif id == 'updateTimestamp': query = self.filter_timestamp(value) qs = qs.filter(query) return qs + def filter_displayname(self, qs, value): + if 'exclusion report'.find(value) != -1: + qs = qs.filter(Q(type__the_type='Exclusion Report')) + elif 'compliance report'.find(value) != -1: + qs = qs.filter(Q(type__the_type='Compliance Report')) + else: + qs = qs.annotate(display_name=Concat(F('type__the_type'), Value(' for '), F('compliance_period__description'))).filter(display_name__icontains=value) + + return qs + def filter_timestamp(self, date): date_query = None date_tuple = date.split('-') @@ -130,46 +164,88 @@ def filter_timestamp(self, date): return date_query def filter_compliance_status(self, qs, value): - if value in 'recommended': - return qs.filter( - (Q(status__manager_status__status__icontains=value) | - Q(status__analyst_status__status__icontains=value)) & - (~Q(status__director_status__status__icontains='Accepted') & - ~Q(status__director_status__status__icontains='Rejected')) - ) - if value in 'supplemental requested': + if 'submitted'.find(value) != -1: + return qs.filter( + Q(status__analyst_status__status='Unreviewed') & + Q(status__director_status__status='Unreviewed') & + Q(status__fuel_supplier_status__status='Submitted') & + Q(status__manager_status__status='Unreviewed') + ) + + if 'accepted'.find(value) != -1: return qs.filter( - Q(status__manager_status__status__icontains=value) | - Q(status__analyst_status__status__icontains=value) + Q(status__director_status__status='Accepted') ) - if value in 'accepted': + + if 'supplemental requested'.find(value) != -1: return qs.filter( - Q(status__director_status__status__icontains=value) + Q(status__manager_status__status='Requested Supplemental') | + Q(status__analyst_status__status='Requested Supplemental') ) - if value in 'rejected': + + if 'rejected'.find(value) != -1: return qs.filter( - Q(status__director_status__status__icontains=value) + Q(status__director_status__status='Rejected') ) - if value in 'recommended acceptance - manager': + + if value == 'recommended acceptance - manager' or 'manager'.find(value) != -1: return qs.filter( - Q(status__manager_status__status__icontains=value) + Q(status__manager_status__status='Recommended') & + ~Q(status__director_status__status='Accepted') & + ~Q(status__director_status__status='Rejected') & + ~Q(status__analyst_status__status='Requested Supplemental') ) - if value in 'recommended rejection - manager': + + if value == 'recommended acceptance - analyst' or 'analyst'.find(value) != -1: return qs.filter( - Q(status__manager_status__status__icontains=value) + Q(status__analyst_status__status='Recommended') & + Q(status__director_status__status='Unreviewed') & + Q(status__manager_status__status='Unreviewed') ) - if value in 'recommended acceptance - analyst': + + if value == 'recommended rejection - manager' or 'rejection'.find(value) != -1: return qs.filter( - Q(status__analyst_status__status__icontains=value) + Q(status__manager_status__status='Not Recommended') ) - if value in 'recommended rejection - analyst': + if value == 'recommended rejection - analyst' or 'rejection'.find(value) != -1: return qs.filter( - Q(status__analyst_status__status__icontains=value) + Q(status__analyst_status__status='Not Recommended') ) - return qs.filter( - Q(status__fuel_supplier_status__status__icontains=value) - ) + + return qs + + def filter_supplemental_status(self, qs, value): + try: + latest_supplementals = self.get_latest_supplemental_reports() + ids = [s.id for s in latest_supplementals] + supplemental_reports = ComplianceReport.objects.filter(id__in=ids) + qs = self.filter_compliance_status(supplemental_reports, value) + except Exception as e: + print(e) + return qs + + def filter_current_status(self, qs, value): + try: + latest_supplementals = self.get_latest_supplemental_reports() + ids = [s.id for s in latest_supplementals] + supplemental_reports = ComplianceReport.objects.filter(id__in=ids) + original_reports = qs.filter(Q(supplemental_reports=None)) + unique_reports = original_reports | supplemental_reports + qs = self.filter_compliance_status(unique_reports, value) + except Exception as e: + print(e) + return qs + + def get_latest_supplemental_reports(self): + latest_supplementals = ComplianceReport.objects.raw(""" + select distinct on (p.id) c.* + from compliance_report p + left join compliance_report c on p.id = c.supplements_id + where c.status_id is not NULL + order by p.id desc, c.create_timestamp desc + """) + return latest_supplementals def get_simple_queryset(self): """ diff --git a/backend/api/viewsets/Document.py b/backend/api/viewsets/Document.py index 29a5703f7..08587cb53 100644 --- a/backend/api/viewsets/Document.py +++ b/backend/api/viewsets/Document.py @@ -112,7 +112,64 @@ def get_queryset(self): ) request = self.request if request.path.endswith('paginated') and request.method == 'POST': - qs = qs.order_by('-id') + sort = request.data.get('sort') + filters = request.data.get('filters') + key_maps = {'title':'title', 'status':'status__status', 'attachment-type':'type__description', 'updateTimestamp': 'update_timestamp', 'organization':'create_user', 'id': 'id', 'credit-transaction-id':'credit_trades'} + if sort: + sortCondition = sort[0].get('desc') + sortId = sort[0].get('id') + sortType = "-" if sortCondition else "" + sortString = f"{sortType}{key_maps[sortId]}" + qs = qs.order_by(sortString) + if filters: + for filter in filters: + id = filter.get('id') + value = filter.get('value') + if id and value: + if id == 'id': + qs = qs.filter(id__icontains = value) + if id == 'organization': + organization_split = value.split() + q_object = None + for x in organization_split: + q_sub_object = Q(create_user__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) + if id =='status': + qs = qs.filter(status__status__icontains = value) + if id == 'attachment-type': + type_split = value.split() + q_object = None + for x in type_split: + q_sub_object = Q(type__description__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) + if id == 'title': + title_split = value.split() + q_object = None + for x in title_split: + q_sub_object = Q(title__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) + if id == 'updateTimestamp': + date_split = value.split("-") + q_object = None + for x in date_split: + q_sub_object = Q(update_timestamp__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) return qs def perform_create(self, serializer): diff --git a/backend/api/viewsets/Notification.py b/backend/api/viewsets/Notification.py index 7b0651934..c010cccaa 100644 --- a/backend/api/viewsets/Notification.py +++ b/backend/api/viewsets/Notification.py @@ -15,7 +15,8 @@ from api.serializers.Notifications import NotificationMessageSerializer from auditable.views import AuditableMixin from api.paginations import BasicPagination -from django.db.models import Q +from django.db.models import Q, F, Value +from django.db.models.functions import Concat class NotificationToken(object): @@ -65,10 +66,25 @@ def get_queryset(self): is_archived=False, user=user ) - + request = self.request if request.path.endswith('processed_list') and request.method == 'POST': - qs = qs.order_by('-create_timestamp') + sort = request.data.get('sort') + key_maps = {'notification':'message', 'date':'create_timestamp', 'creditTrade':'related_credit_trade__id', 'organization':'related_organization__name'} + if sort: + sortCondition = sort[0].get('desc') + sortId = sort[0].get('id') + if sortId=='user': + if sortCondition: + qs = qs = qs.annotate(display_name=Concat(F('originating_user__first_name'), Value(' '), F('originating_user__last_name'))).order_by('-display_name') + else: + qs = qs = qs.annotate(display_name=Concat(F('originating_user__first_name'), Value(' '), F('originating_user__last_name'))).order_by('display_name') + else: + sortType = "-" if sortCondition else "" + sortString = f"{sortType}{key_maps[sortId]}" + qs = qs.order_by(sortString) + else: + qs = qs.order_by('-create_timestamp') filters = request.data.get('filters') if filters: for filter in filters: @@ -77,9 +93,25 @@ def get_queryset(self): if id and value: if id == 'notification': #todo: this can be improved - qs = qs.filter(message__icontains = value) + notification_split = value.split() + q_object = None + for x in notification_split: + q_sub_object = Q(message__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) elif id == 'date': - qs = qs.filter(update_timestamp__icontains = value) + date_split = value.split("-") + q_object = None + for x in date_split: + q_sub_object = Q(update_timestamp__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) elif id == 'user': user_split = value.split() q_object = None @@ -88,7 +120,7 @@ def get_queryset(self): if not q_object: q_object = q_sub_object else: - q_object = q_object | q_sub_object + q_object = q_object & q_sub_object qs = qs.filter(q_object) elif id == 'creditTrade': if value.isnumeric(): @@ -98,7 +130,15 @@ def get_queryset(self): else: qs = qs.none() elif id == 'organization': - qs = qs.filter(related_organization__name__icontains = value) + organization_split = value.split() + q_object = None + for x in organization_split: + q_sub_object = Q(related_organization__name__icontains = x) + if not q_object: + q_object = q_sub_object + else: + q_object = q_object & q_sub_object + qs = qs.filter(q_object) return qs diff --git a/backend/requirements.txt b/backend/requirements.txt index 09f1ddc06..72a1ebcce 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,7 +3,7 @@ asgiref==3.5.2 backports.zoneinfo==0.2.1 billiard==3.5.0.5 celery==4.2.0 -certifi==2022.9.24 +certifi==2022.12.7 cffi==1.15.1 charset-normalizer==2.1.1 coreapi==2.3.3 diff --git a/charts/tfrs-spilo/Chart.yaml b/charts/tfrs-spilo/Chart.yaml index dcfc17b2e..b6c203df2 100644 --- a/charts/tfrs-spilo/Chart.yaml +++ b/charts/tfrs-spilo/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: itvr-spilo -description: A Helm chart for setting up splio for itvr project on Openshift +name: tfrs-spilo +description: A Helm chart for setting up splio for tfrs project on Openshift # A chart can be either an 'application' or a 'library' chart. # diff --git a/charts/tfrs-spilo/Readme.md b/charts/tfrs-spilo/Readme.md index dc6fc5b4c..9ca758e90 100644 --- a/charts/tfrs-spilo/Readme.md +++ b/charts/tfrs-spilo/Readme.md @@ -1,32 +1,82 @@ ## Before running Helm * Create secret tfrs-patroni-admin + * Create the secret by using tfrs/openshift-v4/templates/spilo/tfrs-patroni-admin.yaml, the three passwords are generated randomly + * Create secret tfrs-patroni-app + * Create the secret by using tfrs/openshift-v4/templates/spilo/tfrs-patroni-app.yaml, the three password fields must be in sync with the existing secret patroni-test + * It contains: app-db-name, app-db-password, app-db-username, metabaseuser-name, metabaseuser-password + * The replication- and superuser- are not needed + * If this secret is aleady existed, please verify the password fields + * Create Object Storage secret for database continuous backup, tfrs-object-storage + * Create the secret by using tfrs/openshift-v4/templates/object-storage/object-storage-secret.yaml + * The secret should have been created, verify it by using CyberDuck + +* Create secret tfrs-db-backup-s3 + * It includes AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_ENDPOINT + * The values are in sync with secret tfrs-object-storage + +* Verify values-test.yaml. Create the bucket on object storage if needed + +* Add new KNPs templates/knp/knp-env-pr-new-tfrs-spilo.yaml + * oc process -f ./knp-env-pr-new-tfrs-spilo.yaml ENVIRONMENT=test | oc apply -f - -n 0ab226-test ## Heml command -helm install -n 0ab226-dev -f ./values-dev.yaml tfrs-spilo . -helm uninstall -n 0ab226-dev tfrs-spilo +helm install -n 0ab226-test -f ./values-test.yaml tfrs-spilo . +helm uninstall -n 0ab226-test tfrs-spilo ## Migrate Postgresql 10 on Patroni to 14 on Spilo container + +### Bring down the TFRS application and route the frontend to maintenance mode + +### Run a final backup on backup container + +### Create tfrs database user and database +* Login to the tfrs-spilo leader pod * If the username contains upper case letters, should be double quoted -* create user for tfrs database, the username should be the same on v10 otherwise the restore may encounter issue - * create user [username] with password '[password]' -* create tfrs database - * create database tfrs owner [username] ENCODING 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8' -* login to spilo pods, run the following psql to only keep 24 hours log files, otherwise they take too much space + * create user for tfrs database, the username should be the same on v10 otherwise the restore may encounter issue + * create user [username] with password '[password]' + * The password can be found in secret tfrs-patroni-app + * create tfrs database + * create database tfrs owner [username] ENCODING 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8' +### Reset postgresql logging +* login tfrs-spilo leader pod, run the following psql to only keep 24 hours log files, otherwise they take too much space ALTER SYSTEM SET log_filename='postgresql-%H.log'; ALTER SYSTEM SET log_connections='off'; ALTER SYSTEM SET log_disconnections='off'; ALTER SYSTEM SET log_checkpoints='off'; select pg_reload_conf(); -* Create metabase user +### Create metabase user +* login tfrs-spilo leader pod CREATE USER metabaseuser WITH PASSWORD 'xxxxxx'; - GRANT CONNECT ON DATABASE zeva TO metabaseuser; + GRANT CONNECT ON DATABASE tfrs TO metabaseuser; GRANT USAGE ON SCHEMA public TO metabaseuser; GRANT SELECT ON ALL TABLES IN SCHEMA public TO metabaseuser; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO metabaseuser; verify permissions are granted: select * from information_schema.role_table_grants where grantee='metabaseuser'; -* Backup tfrs database - pg_dump tfrs > tfrs.sql + +## Backup the existing v10 database and restore to v14 cluster +* Make sure the application is stopped +* Login to patroni-test leader pod + * make an empty dir /home/postgres/migration and cd into it + * backup tfrs database: pg_dump tfrs > tfrs.sql * Restore tfrs database - psql tfrs < ./tfrs.sql >> ./restore.log 2>&1 + * psql tfrs < ./tfrs.sql >> ./restore.log 2>&1 + * verify the restore.log when complete + +* Point the applications to v14 cluster, update the enviuronment variables for + * backend: DATABASE_SERVICE_NAME, POSTGRESQL_SERVICE_HOST + * celery: DATABASE_SERVICE_NAME + * scan-handler: DATABASE_SERVICE_NAME +* Bring down the v10 cluster +* Bring down the maintenance page +* Bring up the tfrs appliation +* Update patroni backup to only backup minio data +* Update metabase connection from CTHUB +* Update dbServiceName to be tfrs-spilo in .pipeline/lib/config.js + +## Notes for uninstalling tfrs-spilo when needed +* After the helm uninstall command, remember to remove the followings: + * The two configmaps: tfrs-spilo-config, tfrs-spilo-leader + * The PVCs storage-volume-tfrs-spilo-* + * The backup bucket in object storage diff --git a/charts/tfrs-spilo/values-test.yaml b/charts/tfrs-spilo/values-test.yaml index 8a186b1c2..190628154 100644 --- a/charts/tfrs-spilo/values-test.yaml +++ b/charts/tfrs-spilo/values-test.yaml @@ -5,7 +5,7 @@ spilo: credentials: useExistingSecret: true existingSecret: - name: itvr-patroni-admin + name: tfrs-patroni-admin superuserKey: password-superuser adminKey: password-admin standbyKey: password-standby @@ -16,17 +16,17 @@ spilo: retainBackups: 3 storage: s3 s3: - bucket: itvrts - secretName: itvr-db-backup-s3 + bucket: tfrsts/postgresbackup + secretName: tfrs-db-backup-s3 shipLogs: enabled: false # s3: -# bucket: s3://itvrts +# bucket: s3://tfrsts # shipSchedule: 0 7 * * * persistentVolume: - size: 5Gi + size: 2Gi storageClass: netapp-block-standard resources: diff --git a/docker-compose.yml b/docker-compose.yml index 86c2950e5..b327a5ae9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,121 +1,127 @@ -version: '3' +version: "3" services: - db: - image: postgres - container_name: tfrs_db - environment: - POSTGRES_DB: tfrs - POSTGRES_USER: tfrs - POSTGRES_PASSWORD: development_only - ports: - - 5432:5432 - volumes: - - postgres_data:/var/lib/postgresql/data - django: - environment: - - DATABASE_NAME=tfrs - - DATABASE_USER=tfrs - - DATABASE_PASSWORD=development_only - - DATABASE_ENGINE=postgresql - - DATABASE_SERVICE_NAME=postgresql - - POSTGRESQL_SERVICE_HOST=db - - POSTGRESQL_SERVICE_PORT=5432 - - RABBITMQ_VHOST=/tfrs - - RABBITMQ_USER=rabbitmq - - RABBITMQ_PASSWORD=rabbitmq - - RABBITMQ_HOST=rabbit - - RABBITMQ_PORT=5672 - - KEYCLOAK_ENABLED=True - - KEYCLOAK_AUDIENCE=tfrs-on-gold-4308 - - KEYCLOAK_CLIENT_ID=tfrs-on-gold-4308 - - KEYCLOAK_REALM=standard - - KEYCLOAK_ISSUER=https://dev.loginproxy.gov.bc.ca/auth/realms/standard - - KEYCLOAK_CERTS_URL=https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs - - KEYCLOAK_SA_BASEURL=https://dev.loginproxy.gov.bc.ca - - KEYCLOAK_SA_REALM=tfrs-on-gold-4308 - - KEYCLOAK_SA_CLIENT_ID=tfrs-app-sa - - KEYCLOAK_SA_CLIENT_SECRET=06dc71d6-1800-4f5d-b7b3-4c4fda226599 - - DOCUMENTS_API_ENABLED=True - - SMTP_SERVER_HOST=smtplogger - - SMTP_SERVER_PORT=2500 - - EMAIL_SENDING_ENABLED=True - - EMAIL_FROM_ADDRESS=tfrs-dev@test.local - - FUEL_CODES_API_ENABLED=True - - CREDIT_CALCULATION_API_ENABLED=True - - COMPLIANCE_REPORTING_API_ENABLED=True - env_file: - - minio.env - depends_on: - - db - build: - dockerfile: Dockerfile-django - context: ./backend - command: > - bash -c - "pip install -q -r requirements.txt && - /wfi/wait-for-it.sh -t 14400 rabbit:5672 && - /wfi/wait-for-it.sh -t 14400 db:5432 && - /wfi/wait-for-it.sh -t 14400 minio:9000 && - /wfi/wait-for-it.sh -t 14400 smtplogger:2500 && - python3 manage.py makemigrations && - python3 manage.py migrate && - python3 manage.py createcachetable && - python3 manage.py load_ops_data api/fixtures/development/dockerized.py && - supervisord" - ports: - - 8000:8000 - volumes: - - ./backend:/app - node: - build: - dockerfile: Dockerfile - context: ./frontend - command: > - bash -c - "npm install && npm run start" - depends_on: - - rabbit - ports: - - 3000:3000 - environment: - - RABBITMQ_VHOST=/tfrs - - RABBITMQ_USER=rabbitmq - - RABBITMQ_PASSWORD=rabbitmq - - RABBITMQ_HOST=rabbit - - RABBITMQ_PORT=5672 - volumes: - - ./frontend:/app - - node_modules:/app/node_modules - rabbit: - image: rabbitmq:3.7-management - hostname: "rabbit" - environment: - - RABBITMQ_DEFAULT_USER=rabbitmq - - RABBITMQ_DEFAULT_PASS=rabbitmq - - RABBITMQ_DEFAULT_VHOST=/tfrs - - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit log_levels [{connection,error}] - ports: - - 15672:15672 - - 5672:5672 - minio: - image: minio/minio - volumes: - - minio_data:/export - environment: - MINIO_ROOT_USER: 296e92217fa3479aaf9cc9641fdb6e0a - MINIO_ROOT_PASSWORD: 778eecb24d7743b5a1b56bbf36a29d62 - ports: - - 9000:9000 - command: "server /export" - smtplogger: - build: - context: ./smtplogger - dockerfile: Dockerfile-smtplogger - ports: - - 2500:2500 + db: + platform: linux/amd64 + image: postgres + container_name: tfrs_db + environment: + POSTGRES_DB: tfrs + POSTGRES_USER: tfrs + POSTGRES_PASSWORD: development_only + ports: + - 5432:5432 + volumes: + - postgres_data:/var/lib/postgresql/data + django: + platform: linux/amd64 + environment: + - DATABASE_NAME=tfrs + - DATABASE_USER=tfrs + - DATABASE_PASSWORD=development_only + - DATABASE_ENGINE=postgresql + - DATABASE_SERVICE_NAME=postgresql + - POSTGRESQL_SERVICE_HOST=db + - POSTGRESQL_SERVICE_PORT=5432 + - RABBITMQ_VHOST=/tfrs + - RABBITMQ_USER=rabbitmq + - RABBITMQ_PASSWORD=rabbitmq + - RABBITMQ_HOST=rabbit + - RABBITMQ_PORT=5672 + - KEYCLOAK_ENABLED=True + - KEYCLOAK_AUDIENCE=tfrs-on-gold-4308 + - KEYCLOAK_CLIENT_ID=tfrs-on-gold-4308 + - KEYCLOAK_REALM=standard + - KEYCLOAK_ISSUER=https://dev.loginproxy.gov.bc.ca/auth/realms/standard + - KEYCLOAK_CERTS_URL=https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs + - KEYCLOAK_SA_BASEURL=https://dev.loginproxy.gov.bc.ca + - KEYCLOAK_SA_REALM=tfrs-on-gold-4308 + - KEYCLOAK_SA_CLIENT_ID=tfrs-app-sa + - KEYCLOAK_SA_CLIENT_SECRET=06dc71d6-1800-4f5d-b7b3-4c4fda226599 + - DOCUMENTS_API_ENABLED=True + - SMTP_SERVER_HOST=smtplogger + - SMTP_SERVER_PORT=2500 + - EMAIL_SENDING_ENABLED=True + - EMAIL_FROM_ADDRESS=tfrs-dev@test.local + - FUEL_CODES_API_ENABLED=True + - CREDIT_CALCULATION_API_ENABLED=True + - COMPLIANCE_REPORTING_API_ENABLED=True + env_file: + - minio.env + depends_on: + - db + build: + dockerfile: Dockerfile-django + context: ./backend + command: > + bash -c + "pip install -q -r requirements.txt && + /wfi/wait-for-it.sh -t 14400 rabbit:5672 && + /wfi/wait-for-it.sh -t 14400 db:5432 && + /wfi/wait-for-it.sh -t 14400 minio:9000 && + /wfi/wait-for-it.sh -t 14400 smtplogger:2500 && + python3 manage.py makemigrations && + python3 manage.py migrate && + python3 manage.py createcachetable && + python3 manage.py load_ops_data api/fixtures/development/dockerized.py && + supervisord" + ports: + - 8000:8000 + volumes: + - ./backend:/app + node: + platform: linux/amd64 + build: + dockerfile: Dockerfile + context: ./frontend + command: > + bash -c + "npm install && npm run start" + depends_on: + - rabbit + ports: + - 3000:3000 + environment: + - RABBITMQ_VHOST=/tfrs + - RABBITMQ_USER=rabbitmq + - RABBITMQ_PASSWORD=rabbitmq + - RABBITMQ_HOST=rabbit + - RABBITMQ_PORT=5672 + volumes: + - ./frontend:/app + - node_modules:/app/node_modules + rabbit: + platform: linux/amd64 + image: rabbitmq:3.7-management + hostname: "rabbit" + environment: + - RABBITMQ_DEFAULT_USER=rabbitmq + - RABBITMQ_DEFAULT_PASS=rabbitmq + - RABBITMQ_DEFAULT_VHOST=/tfrs + - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit log_levels [{connection,error}] + ports: + - 15672:15672 + - 5672:5672 + minio: + platform: linux/amd64 + image: minio/minio + volumes: + - minio_data:/export + environment: + MINIO_ROOT_USER: 296e92217fa3479aaf9cc9641fdb6e0a + MINIO_ROOT_PASSWORD: 778eecb24d7743b5a1b56bbf36a29d62 + ports: + - 9000:9000 + command: "server /export" + smtplogger: + platform: linux/amd64 + build: + context: ./smtplogger + dockerfile: Dockerfile-smtplogger + ports: + - 2500:2500 volumes: - node_modules: - postgres_data: - postgres_keycloak_data: - minio_data: + node_modules: + postgres_data: + postgres_keycloak_data: + minio_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 846684668..f540c1ce2 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.16.1 +FROM --platform=linux/amd64 node:14.16.1 WORKDIR /app diff --git a/frontend/package.json b/frontend/package.json index 71e32fd32..83ecaebba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "tfrs", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { "@babel/eslint-parser": "^7.19.1", "@babel/polyfill": "^7.12.1", @@ -22,7 +22,6 @@ "history": "^4.10.1", "http-proxy": "^1.18.1", "isomorphic-fetch": "^3.0.0", - "jquery": "^3.6.1", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^1.12.3", "keycloak-js": "^19.0.3", diff --git a/frontend/src/actions/complianceReporting.js b/frontend/src/actions/complianceReporting.js index 7279dd106..cb943f6fc 100644 --- a/frontend/src/actions/complianceReporting.js +++ b/frontend/src/actions/complianceReporting.js @@ -202,7 +202,8 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { const page = data.page const pageSize = data.pageSize const filters = data.filters - return axios.post(`${this.baseUrl}/paginated?page=${page}&size=${pageSize}`, { filters }) + const sorts = data.sorts + return axios.post(`${this.baseUrl}/paginated?page=${page}&size=${pageSize}`, { filters, sorts }) } * findPaginatedHandler () { diff --git a/frontend/src/actions/documentUploads.js b/frontend/src/actions/documentUploads.js index 408e2ddfb..f173926a6 100644 --- a/frontend/src/actions/documentUploads.js +++ b/frontend/src/actions/documentUploads.js @@ -116,11 +116,12 @@ const deleteDocumentUploadRequestError = error => ({ /* * Get Documents */ -const getDocumentUploads = (pageNumber=1, pageSize=10, filters=[]) => (dispatch) => { +const getDocumentUploads = (pageNumber=1, pageSize=10, filters=[], sort=[]) => (dispatch) => { dispatch(getDocumentUploadRequests()) const url = Routes.BASE_URL + Routes.SECURE_DOCUMENT_UPLOAD.API + '/paginated?page=' + pageNumber + '&size=' + pageSize const data = { - filters + filters, + sort } return axios.post(url, data) .then((response) => { diff --git a/frontend/src/actions/keycloakActions.js b/frontend/src/actions/keycloakActions.js index 5da010e95..eb72f16f7 100644 --- a/frontend/src/actions/keycloakActions.js +++ b/frontend/src/actions/keycloakActions.js @@ -34,18 +34,18 @@ export const resetAuth = () => ({ type: ActionTypes.RESET_AUTH }) -export const loginKeycloakUserSuccess = (idToken, refreshToken, expiry) => ({ - payload: { idToken, refreshToken, expiry }, +export const loginKeycloakUserSuccess = (idToken, refreshToken, expiry, refreshExpiry) => ({ + payload: { idToken, refreshToken, expiry, refreshExpiry }, type: ActionTypes.LOGIN_KEYCLOAK_USER_SUCCESS }) -export const loginKeycloakRefreshSuccess = (refreshToken, expiry) => ({ - payload: { refreshToken, expiry }, +export const loginKeycloakRefreshSuccess = (refreshToken, expiry, refreshExpiry) => ({ + payload: { refreshToken, expiry, refreshExpiry }, type: ActionTypes.LOGIN_KEYCLOAK_REFRESH_SUCCESS }) -export const loginKeycloakSilentRefreshSuccess = (idToken, refreshToken, expiry) => ({ - payload: { idToken, refreshToken, expiry }, +export const loginKeycloakSilentRefreshSuccess = (idToken, refreshToken, expiry, refreshExpiry) => ({ + payload: { idToken, refreshToken, expiry, refreshExpiry }, type: ActionTypes.LOGIN_KEYCLOAK_SILENT_REFRESH_SUCCESS }) diff --git a/frontend/src/actions/notificationActions.js b/frontend/src/actions/notificationActions.js index 5dfc58524..cab423f96 100644 --- a/frontend/src/actions/notificationActions.js +++ b/frontend/src/actions/notificationActions.js @@ -7,11 +7,12 @@ import * as Routes from '../constants/routes' /* * Get Notifications */ -const getNotifications = (pageNumber, pageSize, filters) => (dispatch) => { +const getNotifications = (pageNumber, pageSize, filters, sort) => (dispatch) => { dispatch(getNotificationsRequest()) const url = Routes.BASE_URL + Routes.NOTIFICATIONS.PROCESSED_LIST + '?page=' + pageNumber + '&size=' + pageSize const data = { - filters + filters, + sort } axios.post(url, data) .then((response) => { diff --git a/frontend/src/actions/userActions.js b/frontend/src/actions/userActions.js index cf86d56cf..2b1f6f459 100644 --- a/frontend/src/actions/userActions.js +++ b/frontend/src/actions/userActions.js @@ -41,7 +41,6 @@ const updateUser = (id, payload) => (dispatch) => { } const getLoggedInUser = () => (dispatch) => { - console.log('getLoggedInUser') dispatch(getLoggedInUserRequest()) const url = Routes.BASE_URL + Routes.CURRENT_USER axios.get(url) diff --git a/frontend/src/app/components/Modal.js b/frontend/src/app/components/Modal.js index e690ac006..d290d29c9 100644 --- a/frontend/src/app/components/Modal.js +++ b/frontend/src/app/components/Modal.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import Tooltip from '../../app/components/Tooltip' import * as Lang from '../../constants/langEnUs' -import $ from 'jquery' const bootstrapClassFor = (extraConfirmType) => { switch (extraConfirmType) { diff --git a/frontend/src/app/components/Navbar.js b/frontend/src/app/components/Navbar.js index cece5cb3e..07df2bb72 100644 --- a/frontend/src/app/components/Navbar.js +++ b/frontend/src/app/components/Navbar.js @@ -68,6 +68,8 @@ class Navbar extends Component { const SecondLevelNavigation = (
+
{this.props.loggedInUser.displayName &&
, { @@ -212,7 +218,8 @@ ComplianceReportingContainer.propTypes = { }), getCompliancePeriods: PropTypes.func.isRequired, getComplianceReports: PropTypes.func.isRequired, - loggedInUser: PropTypes.shape().isRequired + loggedInUser: PropTypes.shape().isRequired, + savedState: PropTypes.shape().isRequired } const mapStateToProps = state => ({ @@ -230,7 +237,8 @@ const mapStateToProps = state => ({ item: state.rootReducer.exclusionReports.item, success: state.rootReducer.exclusionReports.success }, - loggedInUser: state.rootReducer.userRequest.loggedInUser + loggedInUser: state.rootReducer.userRequest.loggedInUser, + savedState: state.rootReducer.tableState.savedState }) const mapDispatchToProps = { @@ -238,6 +246,6 @@ const mapDispatchToProps = { createExclusionReport: exclusionReports.create, getCompliancePeriods, getComplianceReports: complianceReporting.findPaginated -}; +} export default connect(mapStateToProps, mapDispatchToProps)(withRouter(ComplianceReportingContainer)) diff --git a/frontend/src/compliance_reporting/ScheduleAssessmentContainer.js b/frontend/src/compliance_reporting/ScheduleAssessmentContainer.js index fea19151b..6d952dc20 100644 --- a/frontend/src/compliance_reporting/ScheduleAssessmentContainer.js +++ b/frontend/src/compliance_reporting/ScheduleAssessmentContainer.js @@ -48,6 +48,21 @@ class ScheduleAssessmentContainer extends Component { return } + let hasDirectorAssessment = false; + if (this.props.complianceReport && ['Accepted'].indexOf(this.props.complianceReport.status.directorStatus) >= 0) { + hasDirectorAssessment = true + } else if (this.props.complianceReport && + this.props.complianceReport.history && + this.props.complianceReport.history.find(h => + (['Accepted'].indexOf(h.status.directorStatus) >= 0)) + ) { + hasDirectorAssessment = true; + } + + if (!this.props.loggedInUser.isGovernmentUser && !hasDirectorAssessment) { + return null; + } + let part2Compliant = 'Did not supply Part 2 fuel' let foundInScheduleB = false let foundInScheduleC = false diff --git a/frontend/src/compliance_reporting/ScheduleBContainer.js b/frontend/src/compliance_reporting/ScheduleBContainer.js index b61ae7df8..0c1bf999e 100644 --- a/frontend/src/compliance_reporting/ScheduleBContainer.js +++ b/frontend/src/compliance_reporting/ScheduleBContainer.js @@ -421,13 +421,22 @@ class ScheduleBContainer extends Component { ) } + // Check for existing scheduleDSheetIndex from records + let scheduleDSheetIndex = null + const scheduleBRecords = props.complianceReport?.scheduleB?.records + if (scheduleBRecords != null) { + const record = scheduleBRecords[row - 2] + scheduleDSheetIndex = record ? record.scheduleDSheetIndex : null + } + const values = { customIntensity: grid[row][SCHEDULE_B.CARBON_INTENSITY_FUEL].value, quantity: grid[row][SCHEDULE_B.QUANTITY].value, fuelClass: grid[row][SCHEDULE_B.FUEL_CLASS].value, fuelCode: grid[row][SCHEDULE_B.FUEL_CODE].value, fuelType: grid[row][SCHEDULE_B.FUEL_TYPE].value, - provisionOfTheAct: grid[row][SCHEDULE_B.PROVISION_OF_THE_ACT].value + provisionOfTheAct: grid[row][SCHEDULE_B.PROVISION_OF_THE_ACT].value, + scheduleD_sheetIndex: scheduleDSheetIndex } const response = ComplianceReportingService.computeCredits(context, values) @@ -504,8 +513,9 @@ class ScheduleBContainer extends Component { grid[row][SCHEDULE_B.FUEL_CODE].dataEditor = Select grid[row][SCHEDULE_B.FUEL_CODE].valueViewer = (cellProps) => { const selectedOption = cellProps.cell.getOptions().find(e => - String(e.id) === String(cellProps.value)) + String(e.id) === String(response.parameters.scheduleD_sheetIndex)) if (selectedOption) { + grid[row][SCHEDULE_B.FUEL_CODE].value = selectedOption.id return {selectedOption.descriptiveName} } return {cellProps.value} diff --git a/frontend/src/compliance_reporting/components/ComplianceReportingPage.js b/frontend/src/compliance_reporting/components/ComplianceReportingPage.js index dfe276ec4..0c8e28984 100644 --- a/frontend/src/compliance_reporting/components/ComplianceReportingPage.js +++ b/frontend/src/compliance_reporting/components/ComplianceReportingPage.js @@ -2,15 +2,15 @@ import React from 'react' import PropTypes from 'prop-types' import FontAwesomeIcon from '@fortawesome/react-fontawesome' -import Loading from '../../app/components/Loading' -import CONFIG from '../../config'; -import * as Lang from '../../constants/langEnUs'; -import PERMISSIONS_COMPLIANCE_REPORT from '../../constants/permissions/ComplianceReport'; -import ComplianceReportingTable from './ComplianceReportingTable'; +import CONFIG from '../../config' +import * as Lang from '../../constants/langEnUs' +import PERMISSIONS_COMPLIANCE_REPORT from '../../constants/permissions/ComplianceReport' +import ComplianceReportingTable from './ComplianceReportingTable' const ComplianceReportingPage = (props) => { - const { isFetching, items, itemsCount } = props.complianceReports; - const isEmpty = items.length === 0; + const { isFetching, items, itemsCount } = props.complianceReports + const isEmpty = items.length === 0 + const filters = props.savedState['compliance-reporting']?.filtered return (

{props.title}

@@ -112,6 +112,7 @@ const ComplianceReportingPage = (props) => { isFetching={isFetching} isEmpty={isEmpty} loggedInUser={props.loggedInUser} + filters={filters} />
) @@ -134,7 +135,8 @@ ComplianceReportingPage.propTypes = { }).isRequired, selectComplianceReport: PropTypes.func.isRequired, showModal: PropTypes.func.isRequired, - title: PropTypes.string.isRequired + title: PropTypes.string.isRequired, + savedState: PropTypes.shape().isRequired } export default ComplianceReportingPage diff --git a/frontend/src/compliance_reporting/components/ComplianceReportingTable.js b/frontend/src/compliance_reporting/components/ComplianceReportingTable.js index 2748036d1..43b4d8f27 100644 --- a/frontend/src/compliance_reporting/components/ComplianceReportingTable.js +++ b/frontend/src/compliance_reporting/components/ComplianceReportingTable.js @@ -8,44 +8,49 @@ import 'react-table/react-table.css' import ReactTable from '../../app/components/StateSavingReactTable' -import COMPLIANCE_REPORTING from '../../constants/routes/ComplianceReporting'; -import EXCLUSION_REPORTS from '../../constants/routes/ExclusionReports'; -import ComplianceReportStatus from './ComplianceReportStatus'; -import { withRouter } from '../../utils/withRouter'; -import { calculatePages} from '../../utils/functions' +import COMPLIANCE_REPORTING from '../../constants/routes/ComplianceReporting' +import EXCLUSION_REPORTS from '../../constants/routes/ExclusionReports' +import ComplianceReportStatus from './ComplianceReportStatus' +import { withRouter } from '../../utils/withRouter' +import { calculatePages } from '../../utils/functions' class ComplianceReportingTable extends Component { - constructor (props) { - super(props); + super(props) this.state = { page: 1, pageSize: 10, - filters: [] + filters: props.filters, + sorts: [] } - this.handlePageChange = this.handlePageChange.bind(this); - this.handlePageSizeChange = this.handlePageSizeChange.bind(this); - this.handleFiltersChange = this.handleFiltersChange.bind(this); + this.handlePageChange = this.handlePageChange.bind(this) + this.handlePageSizeChange = this.handlePageSizeChange.bind(this) + this.handleFiltersChange = this.handleFiltersChange.bind(this) + this.handleSortsChange = this.handleSortsChange.bind(this) } componentDidUpdate (prevProps, prevState) { - if (this.state.page !== prevState.page || this.state.pageSize !== prevState.pageSize || this.state.filters !== prevState.filters) { - this.props.getComplianceReports({page: this.state.page, pageSize: this.state.pageSize, filters: this.state.filters}) + if (this.state.page !== prevState.page || this.state.pageSize !== prevState.pageSize || this.state.filters !== prevState.filters || this.state.sorts !== prevState.sorts) { + this.props.getComplianceReports({ page: this.state.page, pageSize: this.state.pageSize, filters: this.state.filters, sorts: this.state.sorts }) } } handlePageChange (page) { - this.setState({page: page}); + this.setState({ page }) } handlePageSizeChange (pageSize) { - this.setState({pageSize: pageSize}); + this.setState({ pageSize }) } handleFiltersChange (filters) { - this.setState({filters: filters}); + this.setState({ filters }) + } + + handleSortsChange (sorts) { + this.setState({ sorts }) } render () { @@ -104,18 +109,18 @@ class ComplianceReportingTable extends Component { minWidth: 75 }, { accessor: (item) => { - let report = item - const { supplementalReports } = item - - if (supplementalReports.length > 0) { - [report] = supplementalReports - } - - while (report.supplementalReports && report.supplementalReports.length > 0) { - [report] = report.supplementalReports - } - - return ComplianceReportStatus(report) + // Temporarily left commented out for posterity and client feedback + // let report = item + // const { supplementalReports } = item + // if (supplementalReports.length > 0) { + // [report] = supplementalReports + // } + // while (report.supplementalReports && report.supplementalReports.length > 0) { + // [report] = report.supplementalReports + // } + // return ComplianceReportStatus(report) + + return ComplianceReportStatus(item) }, className: 'col-status', Header: 'Current Status', @@ -137,7 +142,7 @@ class ComplianceReportingTable extends Component { ) }] - const filterable = true; + const filterable = true return ( { - this.handlePageChange(pageIndex + 1); + this.handlePageChange(pageIndex + 1) }} onPageSizeChange={(pageSize, pageIndex) => { - this.handlePageChange(1); - this.handlePageSizeChange(pageSize); + this.handlePageChange(1) + this.handlePageSizeChange(pageSize) }} filtered={this.state.filters} onFilteredChange={(filtered, column) => { - this.handlePageChange(1); - this.handleFiltersChange(filtered); + this.handlePageChange(1) + this.handleFiltersChange(filtered) + }} + sorts={this.state.sorts} + onSortedChange={(sorts, column) => { + this.handlePageChange(1) + this.handleSortsChange(sorts) }} /> ) @@ -230,6 +240,6 @@ ComplianceReportingTable.propTypes = { isGovernmentUser: PropTypes.bool }).isRequired, getComplianceReports: PropTypes.func.isRequired -}; +} export default withRouter(ComplianceReportingTable) diff --git a/frontend/src/compliance_reporting/components/ScheduleTabs.js b/frontend/src/compliance_reporting/components/ScheduleTabs.js index 0b3fdd0f4..8c0f89db7 100644 --- a/frontend/src/compliance_reporting/components/ScheduleTabs.js +++ b/frontend/src/compliance_reporting/components/ScheduleTabs.js @@ -19,23 +19,21 @@ const ScheduleTabs = (props) => { let showAssessment = false - if (props.complianceReport.status.directorStatus !== 'Rejected') { - if (props.complianceReport && ['Accepted'].indexOf(props.complianceReport.status.directorStatus) >= 0) { - showAssessment = true - } else if (props.complianceReport && - props.complianceReport.history && - props.complianceReport.history.find(h => - (['Accepted'].indexOf(h.status.directorStatus) >= 0)) - ) { - // at least one prior version was accepted - showAssessment = true - } + if (props.complianceReport && ['Accepted'].indexOf(props.complianceReport.status.directorStatus) >= 0) { + showAssessment = true + } else if (props.complianceReport && + props.complianceReport.history && + props.complianceReport.history.find(h => + (['Accepted'].indexOf(h.status.directorStatus) >= 0)) + ) { + // at least one prior version was accepted + showAssessment = true + } - if (props.loggedInUser.isGovernmentUser && - (['Recommended', 'Not Recommended'].indexOf(props.complianceReport.status.analystStatus) >= 0 || - ['Recommended', 'Not Recommended'].indexOf(props.complianceReport.status.managerStatus) >= 0)) { - showAssessment = true - } + if (props.loggedInUser.isGovernmentUser && + (['Recommended', 'Not Recommended'].indexOf(props.complianceReport.status.analystStatus) >= 0 || + ['Recommended', 'Not Recommended'].indexOf(props.complianceReport.status.managerStatus) >= 0)) { + showAssessment = true } return ( diff --git a/frontend/src/compliance_reporting/services/ComplianceReportingService.js b/frontend/src/compliance_reporting/services/ComplianceReportingService.js index 039cdc77a..aead38619 100644 --- a/frontend/src/compliance_reporting/services/ComplianceReportingService.js +++ b/frontend/src/compliance_reporting/services/ComplianceReportingService.js @@ -116,7 +116,8 @@ class ComplianceReportingService { customIntensity, fuelCode, scheduleDIntensityValue, - quantity + quantity, + scheduleD_sheetIndex } = sourceValues if (!fuelType) { @@ -141,7 +142,8 @@ class ComplianceReportingService { scheduleDSelectionRequired: false, intensityInputRequired: false, singleFuelClassAvailable: false, - singleProvisionAvailable: false + singleProvisionAvailable: false, + scheduleD_sheetIndex: false } } } @@ -211,7 +213,8 @@ class ComplianceReportingService { scheduleDSelectionRequired: false, intensityInputRequired: false, singleFuelClassAvailable: false, - singleProvisionAvailable: false + singleProvisionAvailable: false, + scheduleD_sheetIndex: scheduleD_sheetIndex } } diff --git a/frontend/src/constants/routes/Organizations.js b/frontend/src/constants/routes/Organizations.js index 1824568c4..7b742f6d4 100644 --- a/frontend/src/constants/routes/Organizations.js +++ b/frontend/src/constants/routes/Organizations.js @@ -3,7 +3,7 @@ const BASE_PATH = '/organizations' const ORGANIZATIONS = { ADD_USER: `${BASE_PATH}/view/:organizationId/add-user`, BULLETIN: 'https://www2.gov.bc.ca/assets/gov/farming-natural-resources-and-industry/electricity-alternative-energy/transportation/renewable-low-carbon-fuels/rlcf-013.pdf', - CREDIT_MARKET_REPORT: 'https://www2.gov.bc.ca/assets/gov/farming-natural-resources-and-industry/electricity-alternative-energy/transportation/renewable-low-carbon-fuels/rlcf-017.pdf', + CREDIT_MARKET_REPORT: 'https://www2.gov.bc.ca/gov/content?id=4B2DC59D77F64C8491C5CDFCF8732F10', DETAILS: `${BASE_PATH}/view/:id`, EDIT: `${BASE_PATH}/edit/:id`, EXPORT: `${BASE_PATH}/xls`, diff --git a/frontend/src/credit_transfers/components/CreditTransferDetails.js b/frontend/src/credit_transfers/components/CreditTransferDetails.js index 38a61da61..03d287eb7 100644 --- a/frontend/src/credit_transfers/components/CreditTransferDetails.js +++ b/frontend/src/credit_transfers/components/CreditTransferDetails.js @@ -189,8 +189,10 @@ const CreditTransferDetails = props => ( props.comments.length === 0, BTN_SIGN_1_2: props.fields.terms.filter(term => term.value === true).length < props.signingAuthorityAssertions.items.length, - BTN_SIGN_2_2: props.fields.terms.filter(term => - term.value === true).length < props.signingAuthorityAssertions.items.length + BTN_SIGN_2_2: props.loggedInUser.organization.statusDisplay !== 'Active' ? true : props.fields.terms.filter(term => + term.value === true).length < props.signingAuthorityAssertions.items.length, + organizationName: props.loggedInUser.organization.name, + inactiveSupplier: props.loggedInUser.organization.statusDisplay !== 'Active' } } id={props.id} diff --git a/frontend/src/credit_transfers/components/CreditTransferFormButtons.js b/frontend/src/credit_transfers/components/CreditTransferFormButtons.js index a2e189864..26d5f5c4c 100644 --- a/frontend/src/credit_transfers/components/CreditTransferFormButtons.js +++ b/frontend/src/credit_transfers/components/CreditTransferFormButtons.js @@ -108,10 +108,11 @@ const CreditTransferFormButtons = props => { show={props.isCommenting || props.disabled.BTN_SIGN_2_2} title={props.isCommenting ? Lang.TEXT_COMMENT_DIRTY + : (props.disabled.inactiveSupplier ? props.disabled.organizationName + ' is not currently recognized as an active fuel supplier in TFRS and is not permitted to buy credits. Inactive suppliers are only permitted to sell credits.' : (props.permissions.BTN_SIGN_2_2 ? 'Signing Authority Declaration needs to be accepted' : 'You must be assigned the Signing Authority role in order to sign and send ' + - 'a Credit Transfer Proposal to the Low Carbon Fuels Branch')} + 'a Credit Transfer Proposal to the Low Carbon Fuels Branch'))} >