From b370c4e1e43f1c0beb05d20d5d8681756f295469 Mon Sep 17 00:00:00 2001 From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:59:08 -0800 Subject: [PATCH] Tracking pull request to merge release-1.57.0 to master (#2104) * update Minio to object storage (#2105) * feat: zeva-2093 - add checkbox (#2097) * feat: zeva-2093 - add checkbox * add missing prop type * Added functionality to download email list with organizations attached for filtering (#2107) * fix ZEVA 2091: credit application table filter missing some submitted records (#2094) * fix: adds recommended and checked submissions to the queryset filter if the bceid user searches anything that matches submitted * chore: cleans up code * edit logic location --------- Co-authored-by: tim738745 * Removing lock on generated excel sheet to allow for sorting/filtering (#2108) * fix: zeva-2106 - report history (#2111) * fix: zeva-2098 - credit applications list date display (#2114) * roll back minio changes --------- Co-authored-by: tim738745 <98717409+tim738745@users.noreply.github.com> Co-authored-by: JulianForeman <71847719+JulianForeman@users.noreply.github.com> Co-authored-by: Emily <44536222+emi-hi@users.noreply.github.com> Co-authored-by: tim738745 --- .github/workflows/dev-build.yaml | 10 ++-- .github/workflows/release-build.yaml | 8 +-- backend/api/serializers/sales_submission.py | 17 ++++-- .../api/services/bceid_email_spreadsheet.py | 56 +++++++++++++++++++ backend/api/viewsets/credit_request.py | 12 +++- backend/api/viewsets/user.py | 8 +++ frontend/src/app/components/TextInput.js | 9 ++- frontend/src/app/routes/Users.js | 3 +- .../components/ComplianceHistory.js | 16 +++--- .../components/CreditRequestListTable.js | 8 ++- .../users/components/ActiveUsersListPage.js | 30 +++++++++- frontend/src/vehicles/VehicleEditContainer.js | 3 + .../src/vehicles/components/VehicleForm.js | 24 +++++++- 13 files changed, 175 insertions(+), 29 deletions(-) create mode 100644 backend/api/services/bceid_email_spreadsheet.py diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index e6b018f50..e19cef885 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -1,11 +1,11 @@ ## For each release, please update the value of workflow name, branches and PR_NUMBER ## Also update frontend/package.json version -name: Dev Build 1.56.0 +name: Dev Build 1.57.0 on: push: - branches: [ release-1.56.0 ] + branches: [ release-1.57.0 ] paths: - frontend/** - backend/** @@ -14,8 +14,8 @@ on: env: ## The pull request number of the Tracking pull request to merge the release branch to main - PR_NUMBER: 2088 - VERSION: 1.56.0 + PR_NUMBER: 2104 + VERSION: 1.57.0 GIT_URL: https://github.com/bcgov/zeva.git TOOLS_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-tools DEV_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-dev @@ -32,7 +32,7 @@ jobs: call-unit-test: uses: ./.github/workflows/unit-test-template.yaml with: - pr-number: 2088 + pr-number: 2104 build: diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index e3f55fe28..189f7b9d3 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -1,7 +1,7 @@ ## For each release, please update the value of workflow name, branches and PR_NUMBER ## Also update frontend/package.json version -name: Release Build 1.56.0 +name: Release Build 1.57.0 on: workflow_dispatch: @@ -9,8 +9,8 @@ on: env: ## The pull request number of the Tracking pull request to merge the release branch to main - PR_NUMBER: 2088 - VERSION: 1.56.0 + PR_NUMBER: 2104 + VERSION: 1.57.0 GIT_URL: https://github.com/bcgov/zeva.git TOOLS_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-tools DEV_NAMESPACE: ${{ secrets.OPENSHIFT_NAMESPACE_PLATE }}-dev @@ -27,7 +27,7 @@ jobs: call-unit-test: uses: ./.github/workflows/unit-test-template.yaml with: - pr-number: 2088 + pr-number: 2104 build: diff --git a/backend/api/serializers/sales_submission.py b/backend/api/serializers/sales_submission.py index 15d1f6101..d7d1c02f6 100644 --- a/backend/api/serializers/sales_submission.py +++ b/backend/api/serializers/sales_submission.py @@ -85,6 +85,7 @@ class Meta: class SalesSubmissionBaseListSerializer( ModelSerializer, EnumSupportSerializerMixin, BaseSerializer ): + submission_history_timestamp = SerializerMethodField() submission_history = SerializerMethodField() organization = SerializerMethodField() totals = SerializerMethodField() @@ -92,7 +93,7 @@ class SalesSubmissionBaseListSerializer( total_credits = SerializerMethodField() validation_status = SerializerMethodField() - def get_submission_history(self, obj): + def get_submission_history_timestamp(self, obj): request = self.context.get('request') if obj.part_of_model_year_report: @@ -101,7 +102,7 @@ def get_submission_history(self, obj): ).first() if credit_transaction: - return credit_transaction.transaction_timestamp.date() + return credit_transaction.transaction_timestamp if not request.user.is_government and obj.validation_status in [ SalesSubmissionStatuses.RECOMMEND_REJECTION, @@ -117,7 +118,7 @@ def get_submission_history(self, obj): if history is None: return None - return history.update_timestamp.date() + return history.update_timestamp # return the last updated date history = SalesSubmissionHistory.objects.filter( @@ -128,7 +129,13 @@ def get_submission_history(self, obj): if history is None: return None - return history.update_timestamp.date() + return history.update_timestamp + + def get_submission_history(self, obj): + timestamp = self.get_submission_history_timestamp(obj) + if timestamp: + return timestamp.date() + return None def get_organization(self, obj): return { @@ -213,7 +220,7 @@ def get_total_credits(self, obj): class Meta: model = SalesSubmission fields = [ - 'id', 'submission_history', 'organization', 'totals', 'unselected', + 'id', 'submission_history_timestamp', 'submission_history', 'organization', 'totals', 'unselected', 'total_warnings', 'total_credits', 'validation_status' ] diff --git a/backend/api/services/bceid_email_spreadsheet.py b/backend/api/services/bceid_email_spreadsheet.py new file mode 100644 index 000000000..20164f407 --- /dev/null +++ b/backend/api/services/bceid_email_spreadsheet.py @@ -0,0 +1,56 @@ +from django.http import HttpResponse +import io +import xlwt +from datetime import datetime +from api.models.user_profile import UserProfile + +BOLD = xlwt.easyxf('font: name Times New Roman, bold on;') + +def create_bceid_emails_sheet(): + sheet_name = 'Active BCeID Emails' + + output = io.BytesIO() + workbook = xlwt.Workbook('Emails.xls') + workbook.protect = True + + descriptor = { + 'version': 2, + 'create_time': datetime.utcnow().timestamp(), + 'sheets': [] + } + + worksheet = workbook.add_sheet(sheet_name) + descriptor['sheets'].append({ + 'index': 1, + 'name': sheet_name + }) + + row = 0 + + worksheet.write(row, 0, 'Organization', style=BOLD) + worksheet.write(row, 1, 'Email', style=BOLD) + + users = UserProfile.objects.filter(is_active=True).exclude(organization__is_government='True').values('organization__name', 'email') + + users_list = list(users) + + for user in users_list: + row += 1 + if user['email'] and user['organization__name']: + worksheet.write(row, 0, user['organization__name']) + worksheet.write(row, 1, user['email']) + + org_col = worksheet.col(0) + org_col.width = 256 * 30 + + email_col = worksheet.col(1) + email_col.width = 256 * 30 # 30 characters for VIN + + workbook.save(output) + output.seek(0) + + filename = 'active_bceid_users.xls' + response = HttpResponse(output.getvalue(), content_type='application/vnd.ms-excel') + response['Content-Disposition'] = f'attachment; filename="{filename}"' + + return response \ No newline at end of file diff --git a/backend/api/viewsets/credit_request.py b/backend/api/viewsets/credit_request.py index d1132653f..907400695 100644 --- a/backend/api/viewsets/credit_request.py +++ b/backend/api/viewsets/credit_request.py @@ -131,7 +131,7 @@ def paginated(self, request): if q_obj: queryset = queryset.filter(q_obj) elif id == "status": - queryset = self.filter_by_status(queryset, search_type, search_terms) + queryset = self.filter_by_status(queryset, search_type, search_terms, request.user) for sort in sorts: id = sort.get("id") desc = sort.get("desc") @@ -174,7 +174,7 @@ def paginated(self, request): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - def filter_by_status(self, queryset, search_type, search_terms): + def filter_by_status(self, queryset, search_type, search_terms, user): mappings = { "validated": SalesSubmissionStatuses.CHECKED.value, "issued": SalesSubmissionStatuses.VALIDATED.value, @@ -206,6 +206,14 @@ def filter_by_status(self, queryset, search_type, search_terms): else: contains_unmapped_search_term = True + if not user.is_government and SalesSubmissionStatuses.SUBMITTED.value in mapped_search_terms: + statuses_to_add = [ + SalesSubmissionStatuses.CHECKED.value, + SalesSubmissionStatuses.RECOMMEND_APPROVAL.value, + SalesSubmissionStatuses.RECOMMEND_REJECTION.value + ] + mapped_search_terms.extend(statuses_to_add) + final_q = get_search_q_object( mapped_search_terms, "exact", diff --git a/backend/api/viewsets/user.py b/backend/api/viewsets/user.py index 27dba01e7..1b737a079 100644 --- a/backend/api/viewsets/user.py +++ b/backend/api/viewsets/user.py @@ -5,6 +5,7 @@ from api.models.user_profile import UserProfile from api.permissions.user import UserPermissions from api.serializers.user import UserSerializer, UserSaveSerializer +from api.services.bceid_email_spreadsheet import create_bceid_emails_sheet from auditable.views import AuditableMixin @@ -42,3 +43,10 @@ def current(self, request): serializer = self.get_serializer(request.user) return Response(serializer.data) + + @action(detail=False) + def download_active(self, request): + + active_bceid_users_excel = create_bceid_emails_sheet() + + return active_bceid_users_excel diff --git a/frontend/src/app/components/TextInput.js b/frontend/src/app/components/TextInput.js index c49187b2e..52724ce6f 100644 --- a/frontend/src/app/components/TextInput.js +++ b/frontend/src/app/components/TextInput.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' const TextInput = (props) => { const { + additionalClasses, defaultValue, errorMessage, handleInputChange, @@ -59,8 +60,13 @@ const TextInput = (props) => { } } + let className = rowClass + if (additionalClasses) { + className = `${rowClass} ${additionalClasses}` + } + return ( -
+
@@ -112,6 +118,7 @@ TextInput.defaultProps = { } TextInput.propTypes = { + additionalClasses: PropTypes.string, defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), details: PropTypes.string, errorMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), diff --git a/frontend/src/app/routes/Users.js b/frontend/src/app/routes/Users.js index c6f360436..9a873aa61 100644 --- a/frontend/src/app/routes/Users.js +++ b/frontend/src/app/routes/Users.js @@ -5,7 +5,8 @@ const USERS = { ME: `${API_BASE_PATH}/current`, DETAILS: `${API_BASE_PATH}/:id`, EDIT: `${API_BASE_PATH}/:id/edit`, - ACTIVE: `${API_BASE_PATH}/active` + ACTIVE: `${API_BASE_PATH}/active`, + DOWNLOAD_ACTIVE: `${API_BASE_PATH}/download_active` } export default USERS diff --git a/frontend/src/compliance/components/ComplianceHistory.js b/frontend/src/compliance/components/ComplianceHistory.js index 7b3d75ac5..10654c0f2 100644 --- a/frontend/src/compliance/components/ComplianceHistory.js +++ b/frontend/src/compliance/components/ComplianceHistory.js @@ -61,7 +61,14 @@ const ComplianceHistory = (props) => { return result } - const getHistory = (itemHistory, item) => { + const getHistory = (history, item) => { + let itemHistory = history + if (itemHistory) { + const sequentialStatusesToRemove = ['DRAFT', 'SUBMITTED', 'RECOMMENDED', 'RETURNED'] + sequentialStatusesToRemove.forEach((status) => { + itemHistory = removeSequentialHistoryItems(itemHistory, status) + }) + } const tempHistory = [] if (itemHistory) { itemHistory.forEach((obj, i) => { @@ -92,12 +99,7 @@ const ComplianceHistory = (props) => { }) } - let result = tempHistory - const sequentialStatusesToRemove = ['DRAFT', 'SUBMITTED', 'RECOMMENDED'] - sequentialStatusesToRemove.forEach((status) => { - result = removeSequentialHistoryItems(result, status) - }) - return result + return tempHistory } const getTitle = (item) => { const type = item.isSupplementary ? startedAsSupplemental ? 'Supplementary Report' : 'Reassessment' : 'Model Year Report' diff --git a/frontend/src/credits/components/CreditRequestListTable.js b/frontend/src/credits/components/CreditRequestListTable.js index 52a08241e..43a266f5a 100644 --- a/frontend/src/credits/components/CreditRequestListTable.js +++ b/frontend/src/credits/components/CreditRequestListTable.js @@ -12,6 +12,7 @@ import history from '../../app/History' import ROUTES_CREDIT_REQUESTS from '../../app/routes/CreditRequests' import calculateNumberOfPages from '../../app/utilities/calculateNumberOfPages' import CustomFilterComponent from '../../app/components/CustomFilterComponent' +import moment from 'moment-timezone' const CreditRequestListTable = (props) => { const { @@ -55,7 +56,12 @@ const CreditRequestListTable = (props) => { }, { id: 'date', - accessor: 'submissionHistory', + accessor: (item) => { + if(item.submissionHistoryTimestamp) { + return moment(item.submissionHistoryTimestamp).format('YYYY-MM-DD') + } + return '' + }, className: 'text-center', Header: 'Date', maxWidth: 150, diff --git a/frontend/src/users/components/ActiveUsersListPage.js b/frontend/src/users/components/ActiveUsersListPage.js index 0306a269c..f2a2f7fed 100644 --- a/frontend/src/users/components/ActiveUsersListPage.js +++ b/frontend/src/users/components/ActiveUsersListPage.js @@ -1,10 +1,30 @@ import React from 'react' import PropTypes from 'prop-types' import Loading from '../../app/components/Loading' +import axios from 'axios' +import ROUTES_USERS from '../../app/routes/Users' +import Button from '../../app/components/Button' const ActiveUsersListPage = (props) => { const { activeIdirUsers, activeBceidUsers, loading } = props + const handleDownload = async () => { + try { + const response = await axios.get(ROUTES_USERS.DOWNLOAD_ACTIVE, {responseType: 'blob'}) + + const url = window.URL.createObjectURL(new Blob([response.data])) + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', 'Active_BCEID_Users.xls'); // File name for download + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + console.error('Error downloading the file: ', error) + } + + } + if (loading) { return } @@ -14,7 +34,7 @@ const ActiveUsersListPage = (props) => {

Active Users

-
+

BCEID Users