From 6a5aa687d30a5fa2ba2dbf24b4fedc26cd5b6ba4 Mon Sep 17 00:00:00 2001 From: John Willis Date: Fri, 2 Jul 2021 17:29:30 -0400 Subject: [PATCH 1/9] Submit data files to actual API, started on updating retrieval --- tdrs-frontend/src/actions/reports.js | 13 ++++--- .../src/components/FileUpload/FileUpload.jsx | 2 ++ .../components/UploadReport/UploadReport.jsx | 35 ++++++++++++------- tdrs-frontend/src/mirage.js | 21 ----------- tdrs-frontend/src/reducers/reports.js | 10 ++++-- 5 files changed, 42 insertions(+), 39 deletions(-) diff --git a/tdrs-frontend/src/actions/reports.js b/tdrs-frontend/src/actions/reports.js index 23c040d03..378f10004 100644 --- a/tdrs-frontend/src/actions/reports.js +++ b/tdrs-frontend/src/actions/reports.js @@ -34,13 +34,17 @@ export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( type: FETCH_FILE_LIST, }) try { - const response = await axios.get(`/mock_api/reports/${year}/${quarter}`, { - responseType: 'json', - }) + const response = await axios.get( + `${process.env.REACT_APP_BACKEND_URL}/reports/`, + { + responseType: 'json', + } + ) + console.log(response.data) dispatch({ type: SET_FILE_LIST, payload: { - data: response.data, + data: response.data.results, }, }) } catch (error) { @@ -103,6 +107,7 @@ export const upload = ({ file, section }) => async (dispatch) => { dispatch({ type: SET_FILE, payload: { + file: file, fileName: file.name, fileType: file.type, section, diff --git a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx index d803a5005..2b492a24a 100644 --- a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx +++ b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx @@ -141,6 +141,8 @@ function FileUpload({ section, setLocalAlertState }) { } }) + console.log({ file }) + // At this point we can reasonably conclude the file is a text file. // Add the file to the redux state dispatch( diff --git a/tdrs-frontend/src/components/UploadReport/UploadReport.jsx b/tdrs-frontend/src/components/UploadReport/UploadReport.jsx index add93d2f9..533ed74fa 100644 --- a/tdrs-frontend/src/components/UploadReport/UploadReport.jsx +++ b/tdrs-frontend/src/components/UploadReport/UploadReport.jsx @@ -78,21 +78,32 @@ function UploadReport({ handleCancel, header, stt }) { return } - const uploadRequests = filteredFiles.map((file) => - axiosInstance.post( + console.log({ filteredFiles }) + + const uploadRequests = filteredFiles.map((file) => { + const formData = new FormData() + const dataFile = { + file: file.file, + original_filename: file.fileName, + slug: file.uuid, + user: user.id, + year: selectedYear, + stt, + quarter: selectedQuarter, + section: file.section, + } + for (const [key, value] of Object.entries(dataFile)) { + formData.append(key, value) + } + return axiosInstance.post( `${process.env.REACT_APP_BACKEND_URL}/reports/`, + formData, { - original_filename: file.fileName, - slug: file.uuid, - user: user.id, - year: selectedYear, - stt, - quarter: selectedQuarter, - section: file.section, - }, - { withCredentials: true } + headers: { 'Content-Type': 'multipart/form-data' }, + withCredentials: true, + } ) - ) + }) Promise.all(uploadRequests) .then((responses) => { diff --git a/tdrs-frontend/src/mirage.js b/tdrs-frontend/src/mirage.js index 4d23f620a..7c3ea4c39 100644 --- a/tdrs-frontend/src/mirage.js +++ b/tdrs-frontend/src/mirage.js @@ -10,27 +10,6 @@ export default function startMirage( routes() { this.namespace = 'mock_api' - this.post('/reports/', () => { - return 'Success' - }) - this.get('/reports/data-files/:year/:quarter/:section', () => { - return 'some text' - }) - this.get('/reports/:year/:quarter/', () => { - return [ - { - fileName: 'test.txt', - section: 'Active Case Data', - uuid: uuidv4(), - }, - { - fileName: 'testb.txt', - section: 'Closed Case Data', - uuid: uuidv4(), - }, - ] - }) - // Allow unhandled requests to pass through this.passthrough(`${process.env.REACT_APP_BACKEND_URL}/**`) this.passthrough(`${process.env.REACT_APP_BACKEND_HOST}/**`) diff --git a/tdrs-frontend/src/reducers/reports.js b/tdrs-frontend/src/reducers/reports.js index 1ca6466d9..ff0d6126f 100644 --- a/tdrs-frontend/src/reducers/reports.js +++ b/tdrs-frontend/src/reducers/reports.js @@ -22,6 +22,7 @@ export const fileUploadSections = [ ] export const getUpdatedFiles = ( + file, state, fileName, section, @@ -33,6 +34,7 @@ export const getUpdatedFiles = ( const oldFileIndex = getFileIndex(state.files, section) const updatedFiles = [...state.files] updatedFiles[oldFileIndex] = { + file, section, fileName, data, @@ -61,8 +63,9 @@ const reports = (state = initialState, action) => { const { type, payload = {} } = action switch (type) { case SET_FILE: { - const { fileName, section, uuid, fileType } = payload + const { file, fileName, section, uuid, fileType } = payload const updatedFiles = getUpdatedFiles( + file, state, fileName, section, @@ -76,6 +79,7 @@ const reports = (state = initialState, action) => { return { ...state, files: state.files.map((file) => { + console.log({ data, section: file.section }) const dataFile = getFile(data, file.section) if (dataFile) { return dataFile @@ -85,12 +89,13 @@ const reports = (state = initialState, action) => { } case CLEAR_FILE: { const { section } = payload - const updatedFiles = getUpdatedFiles(state, null, section, null) + const updatedFiles = getUpdatedFiles(null, state, null, section, null) return { ...state, files: updatedFiles } } case SET_FILE_ERROR: { const { error, section } = payload const updatedFiles = getUpdatedFiles( + null, state, null, section, @@ -104,6 +109,7 @@ const reports = (state = initialState, action) => { const { section } = payload const file = getFile(state.files, section) const updatedFiles = getUpdatedFiles( + file, state, file.fileName, section, From fcd6c61dee4fefd10ce5ff9a7c4538329b06aac8 Mon Sep 17 00:00:00 2001 From: John Willis Date: Tue, 6 Jul 2021 17:21:02 -0400 Subject: [PATCH 2/9] Implemented filtering for ReportFile viewset; update redux reducer for SET_FILE_LIST to correctly rename API fields to frontend expected state variable names --- tdrs-backend/tdpservice/reports/views.py | 20 +++++++++++++++++++- tdrs-backend/tdpservice/settings/common.py | 5 ++++- tdrs-backend/tdpservice/users/permissions.py | 2 +- tdrs-frontend/src/actions/reports.js | 3 +-- tdrs-frontend/src/reducers/reports.js | 12 ++++++++---- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/tdrs-backend/tdpservice/reports/views.py b/tdrs-backend/tdpservice/reports/views.py index c950f4373..c37caedfb 100644 --- a/tdrs-backend/tdpservice/reports/views.py +++ b/tdrs-backend/tdpservice/reports/views.py @@ -12,7 +12,7 @@ from tdpservice.reports.serializers import ReportFileSerializer from tdpservice.reports.models import ReportFile -from tdpservice.users.permissions import ReportFilePermissions +from tdpservice.users.permissions import ReportFilePermissions, is_in_group logger = logging.getLogger() @@ -21,11 +21,29 @@ class ReportFileViewSet(ModelViewSet): """Report file views.""" http_method_names = ['get', 'post', 'head'] + filterset_fields = ['year', 'quarter'] parser_classes = [MultiPartParser] permission_classes = [ReportFilePermissions] serializer_class = ReportFileSerializer + + # TODO: Handle versioning in queryset + # Ref: https://github.com/raft-tech/TANF-app/issues/1007 queryset = ReportFile.objects.all() + # NOTE: This is a temporary hack to make sure the latest version of the file + # is the one presented in the UI. Once we implement the above linked issue + # we will be able to appropriately refer to the latest versions only. + ordering = ['-version'] + + def get_queryset(self): + user = self.request.user + + # Ensure Data Preppers can only see reports for their STT + if is_in_group(user, 'Data Prepper'): + return self.queryset.filter(stt_id=user.stt_id) + + return self.queryset + @action(methods=["get"], detail=True) def download(self, request, pk=None): """Retrieve a file from s3 then stream it to the client.""" diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index 99702dafb..8e7e48c45 100755 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -152,7 +152,7 @@ class Common(Configuration): } # General - APPEND_SLASH = False + APPEND_SLASH = True TIME_ZONE = "UTC" LANGUAGE_CODE = "en-us" # If you set this to False, Django will make some optimizations so as not @@ -300,6 +300,9 @@ class Common(Configuration): "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", ), + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + ], "TEST_REQUEST_DEFAULT_FORMAT": "json", "TEST_REQUEST_RENDERER_CLASSES": [ "rest_framework.renderers.MultiPartRenderer", diff --git a/tdrs-backend/tdpservice/users/permissions.py b/tdrs-backend/tdpservice/users/permissions.py index 1a440c13a..4872dfd7f 100644 --- a/tdrs-backend/tdpservice/users/permissions.py +++ b/tdrs-backend/tdpservice/users/permissions.py @@ -76,7 +76,7 @@ def has_object_permission(self, request, view, obj): This is used in cases where we call .get_object() to retrieve a report and do not have the STT available in the request, ie. report was requested for download via the ID of the report. This is not called - on POST requests (creating new reports). + on POST requests (creating new reports) or for a list of reports. """ is_ofa_admin = is_in_group(request.user, "OFA Admin") is_data_prepper = is_in_group(request.user, 'Data Prepper') diff --git a/tdrs-frontend/src/actions/reports.js b/tdrs-frontend/src/actions/reports.js index 378f10004..7431a75fa 100644 --- a/tdrs-frontend/src/actions/reports.js +++ b/tdrs-frontend/src/actions/reports.js @@ -35,12 +35,11 @@ export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( }) try { const response = await axios.get( - `${process.env.REACT_APP_BACKEND_URL}/reports/`, + `${process.env.REACT_APP_BACKEND_URL}/reports/?year=${year}&quarter=${quarter}`, { responseType: 'json', } ) - console.log(response.data) dispatch({ type: SET_FILE_LIST, payload: { diff --git a/tdrs-frontend/src/reducers/reports.js b/tdrs-frontend/src/reducers/reports.js index ff0d6126f..f830bcd08 100644 --- a/tdrs-frontend/src/reducers/reports.js +++ b/tdrs-frontend/src/reducers/reports.js @@ -79,11 +79,15 @@ const reports = (state = initialState, action) => { return { ...state, files: state.files.map((file) => { - console.log({ data, section: file.section }) const dataFile = getFile(data, file.section) - if (dataFile) { - return dataFile - } else return file + return dataFile + ? { + fileName: dataFile.original_filename, + fileType: dataFile.extension, + section: dataFile.section, + uuid: dataFile.slug, + } + : file }), } } From 395d97f16bd3b9dc6ac519581f5c8da3959bb628 Mon Sep 17 00:00:00 2001 From: John Willis Date: Wed, 7 Jul 2021 18:18:51 -0400 Subject: [PATCH 3/9] Hooked up download endpoint to the frontend; implemented CLEAR_FILE_LIST reducer to fix a bug where reports would show for the wrong years --- tdrs-backend/tdpservice/reports/views.py | 8 +++++- tdrs-frontend/src/actions/reports.js | 25 ++++++++++------- .../src/components/FileUpload/FileUpload.jsx | 27 +++++++++---------- .../src/components/Reports/Reports.jsx | 8 +++++- .../components/UploadReport/UploadReport.jsx | 6 +++-- tdrs-frontend/src/reducers/reports.js | 9 ++++++- 6 files changed, 54 insertions(+), 29 deletions(-) diff --git a/tdrs-backend/tdpservice/reports/views.py b/tdrs-backend/tdpservice/reports/views.py index c37caedfb..7820736cc 100644 --- a/tdrs-backend/tdpservice/reports/views.py +++ b/tdrs-backend/tdpservice/reports/views.py @@ -36,13 +36,19 @@ class ReportFileViewSet(ModelViewSet): ordering = ['-version'] def get_queryset(self): + """Determine the queryset used to fetch records for users.""" user = self.request.user + # OFA Admins can see reports for all STTs + if is_in_group(user, 'OFA Admin'): + return self.queryset + # Ensure Data Preppers can only see reports for their STT if is_in_group(user, 'Data Prepper'): return self.queryset.filter(stt_id=user.stt_id) - return self.queryset + # If a user doesn't belong to either of these groups return no reports + return self.queryset.none() @action(methods=["get"], detail=True) def download(self, request, pk=None): diff --git a/tdrs-frontend/src/actions/reports.js b/tdrs-frontend/src/actions/reports.js index 7431a75fa..4f7b67fdb 100644 --- a/tdrs-frontend/src/actions/reports.js +++ b/tdrs-frontend/src/actions/reports.js @@ -3,8 +3,11 @@ import { logErrorToServer } from '../utils/eventLogger' import { v4 as uuidv4 } from 'uuid' import axios from 'axios' +const BACKEND_URL = process.env.REACT_APP_BACKEND_URL + export const SET_FILE = 'SET_FILE' export const CLEAR_FILE = 'CLEAR_FILE' +export const CLEAR_FILE_LIST = 'CLEAR_FILE_LIST' export const SET_FILE_ERROR = 'SET_FILE_ERROR' export const CLEAR_ERROR = 'CLEAR_ERROR' @@ -20,9 +23,14 @@ export const clearFile = ({ section }) => (dispatch) => { dispatch({ type: CLEAR_FILE, payload: { section } }) } +export const clearFileList = () => (dispatch) => { + dispatch({ type: CLEAR_FILE_LIST }) +} + export const clearError = ({ section }) => (dispatch) => { dispatch({ type: CLEAR_ERROR, payload: { section } }) } + /** Get a list of files that can be downloaded, mainly used to decide if the download button should be present. @@ -35,7 +43,7 @@ export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( }) try { const response = await axios.get( - `${process.env.REACT_APP_BACKEND_URL}/reports/?year=${year}&quarter=${quarter}`, + `${BACKEND_URL}/reports/?year=${year}&quarter=${quarter}`, { responseType: 'json', } @@ -43,7 +51,7 @@ export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( dispatch({ type: SET_FILE_LIST, payload: { - data: response.data.results, + data: response?.data?.results, }, }) } catch (error) { @@ -58,19 +66,16 @@ export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( } } -export const download = ({ year, quarter = 'Q1', section }) => async ( +export const download = ({ id, quarter = 'Q1', section, year }) => async ( dispatch ) => { try { - if (!year) throw new Error('No year was provided to download action.') + if (!id) throw new Error('No id was provided to download action.') dispatch({ type: START_FILE_DOWNLOAD }) - const response = await axios.get( - `/mock_api/reports/data-files/${year}/${quarter}/${section}`, - { - responseType: 'blob', - } - ) + const response = await axios.get(`${BACKEND_URL}/reports/${id}/download/`, { + responseType: 'blob', + }) const data = response.data // Create a link and associate it with the blob returned from the file diff --git a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx index 585c1b4b1..e843c539c 100644 --- a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx +++ b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx @@ -21,16 +21,17 @@ const INVALID_FILE_ERROR = function FileUpload({ section, setLocalAlertState }) { // e.g. 'Aggregate Case Data' => 'aggregate-case-data' // The set of uploaded files in our Redux state - const { files, year } = useSelector((state) => state.reports) + const { files, quarter, year } = useSelector((state) => state.reports) const dispatch = useDispatch() // e.g. "1 - Active Case Data" => ["1", "Active Case Data"] const [sectionNumber, sectionName] = section.split(' - ') - const hasFile = files?.some((file) => { - return file.section === sectionName && file.uuid - }) + console.log({ year, quarter }) + const hasFile = files?.some( + (file) => file.section === sectionName && file.uuid + ) const selectedFile = files.find((file) => sectionName === file.section) @@ -60,7 +61,7 @@ function FileUpload({ section, setLocalAlertState }) { const downloadFile = ({ target }) => { dispatch(clearError({ section: sectionName })) - dispatch(download({ section: sectionName, year })) + dispatch(download(selectedFile)) } const inputRef = useRef(null) @@ -71,13 +72,13 @@ function FileUpload({ section, setLocalAlertState }) { message: null, }) - const { name } = event.target + const { name: section } = event.target const file = event.target.files[0] // Clear existing errors and the current // file in the state if the user is re-uploading - dispatch(clearError({ section: name })) - dispatch(clearFile({ section: name })) + dispatch(clearError({ section })) + dispatch(clearFile({ section })) // Get the the first 4 bytes of the file with which to check file signatures const blob = file.slice(0, 4) @@ -115,7 +116,7 @@ function FileUpload({ section, setLocalAlertState }) { type: SET_FILE_ERROR, payload: { error: { message: INVALID_FILE_ERROR }, - section: name, + section, }, }) return @@ -135,19 +136,17 @@ function FileUpload({ section, setLocalAlertState }) { type: SET_FILE_ERROR, payload: { error: { message: INVALID_FILE_ERROR }, - section: name, + section, }, }) } }) - console.log({ file }) - // At this point we can reasonably conclude the file is a text file. // Add the file to the redux state dispatch( upload({ - section: name, + section, file, }) ) @@ -196,7 +195,7 @@ function FileUpload({ section, setLocalAlertState }) { data-errormessage={INVALID_FILE_ERROR} />
- {hasFile ? ( + {hasFile && selectedFile?.id ? ( -
diff --git a/tdrs-frontend/src/reducers/reports.js b/tdrs-frontend/src/reducers/reports.js index 263dd726c..ad3aeb01e 100644 --- a/tdrs-frontend/src/reducers/reports.js +++ b/tdrs-frontend/src/reducers/reports.js @@ -22,23 +22,23 @@ export const fileUploadSections = [ 'Stratum Data', ] -export const getUpdatedFiles = ( - file, +export const getUpdatedFiles = ({ state, fileName, section, + id = null, uuid = null, fileType = null, error = null, - data = '' -) => { + file = null, +}) => { const oldFileIndex = getFileIndex(state.files, section) const updatedFiles = [...state.files] updatedFiles[oldFileIndex] = { + id, file, section, fileName, - data, error, uuid, fileType, @@ -65,14 +65,14 @@ const reports = (state = initialState, action) => { switch (type) { case SET_FILE: { const { file, fileName, section, uuid, fileType } = payload - const updatedFiles = getUpdatedFiles( - file, + const updatedFiles = getUpdatedFiles({ state, fileName, section, uuid, - fileType - ) + fileType, + file, + }) return { ...state, files: updatedFiles } } case SET_FILE_LIST: { @@ -97,7 +97,7 @@ const reports = (state = initialState, action) => { } case CLEAR_FILE: { const { section } = payload - const updatedFiles = getUpdatedFiles(null, state, null, section, null) + const updatedFiles = getUpdatedFiles({ state, section }) return { ...state, files: updatedFiles } } case CLEAR_FILE_LIST: { @@ -105,28 +105,19 @@ const reports = (state = initialState, action) => { } case SET_FILE_ERROR: { const { error, section } = payload - const updatedFiles = getUpdatedFiles( - null, - state, - null, - section, - null, - null, - error - ) + const updatedFiles = getUpdatedFiles({ state, section, error }) return { ...state, files: updatedFiles } } case CLEAR_ERROR: { const { section } = payload const file = getFile(state.files, section) - const updatedFiles = getUpdatedFiles( - file, + const updatedFiles = getUpdatedFiles({ state, - file.fileName, + fileName: file.fileName, section, - file.uuid, - file.fileType - ) + uuid: file.uuid, + fileType: file.fileType, + }) return { ...state, files: updatedFiles } } case SET_SELECTED_YEAR: { diff --git a/tdrs-frontend/src/utils/removeFileInputErrorState.js b/tdrs-frontend/src/utils/removeFileInputErrorState.js new file mode 100644 index 000000000..a4f8fd449 --- /dev/null +++ b/tdrs-frontend/src/utils/removeFileInputErrorState.js @@ -0,0 +1,11 @@ +/** + * TODO: Add docstring + */ +const removeFileInputErrorState = () => { + const errors = document.querySelectorAll('.has-invalid-file') + for (const error of errors) { + error?.classList?.remove('has-invalid-file') + } +} + +export default removeFileInputErrorState From 9936fafcec253c03539f617ed37018418dda9b0d Mon Sep 17 00:00:00 2001 From: John Willis Date: Fri, 9 Jul 2021 19:24:34 -0400 Subject: [PATCH 5/9] Endpoint filtering improvements; fix ability to select different STT for OFA Admin --- tdrs-backend/tdpservice/reports/views.py | 31 ++++++++++--------- tdrs-backend/tdpservice/users/permissions.py | 17 +++++++++- tdrs-frontend/src/actions/reports.js | 4 +-- .../src/components/Reports/Reports.jsx | 6 +++- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/tdrs-backend/tdpservice/reports/views.py b/tdrs-backend/tdpservice/reports/views.py index 7820736cc..5dba12a99 100644 --- a/tdrs-backend/tdpservice/reports/views.py +++ b/tdrs-backend/tdpservice/reports/views.py @@ -2,6 +2,7 @@ import logging from django.http import StreamingHttpResponse +from django_filters import rest_framework as filters from rest_framework.parsers import MultiPartParser from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST @@ -17,11 +18,20 @@ logger = logging.getLogger() +class ReportFileFilter(filters.FilterSet): + """Filters that can be applied to GET requests as query parameters.""" + stt = filters.NumberFilter(field_name='stt_id', required=True) + + class Meta: + model = ReportFile + fields = ['stt', 'quarter', 'year'] + + class ReportFileViewSet(ModelViewSet): """Report file views.""" http_method_names = ['get', 'post', 'head'] - filterset_fields = ['year', 'quarter'] + filterset_class = ReportFileFilter parser_classes = [MultiPartParser] permission_classes = [ReportFilePermissions] serializer_class = ReportFileSerializer @@ -35,20 +45,11 @@ class ReportFileViewSet(ModelViewSet): # we will be able to appropriately refer to the latest versions only. ordering = ['-version'] - def get_queryset(self): - """Determine the queryset used to fetch records for users.""" - user = self.request.user - - # OFA Admins can see reports for all STTs - if is_in_group(user, 'OFA Admin'): - return self.queryset - - # Ensure Data Preppers can only see reports for their STT - if is_in_group(user, 'Data Prepper'): - return self.queryset.filter(stt_id=user.stt_id) - - # If a user doesn't belong to either of these groups return no reports - return self.queryset.none() + def filter_queryset(self, queryset): + """Only apply filters to the list action.""" + if self.action != 'list': + self.filterset_class = None + return super().filter_queryset(queryset) @action(methods=["get"], detail=True) def download(self, request, pk=None): diff --git a/tdrs-backend/tdpservice/users/permissions.py b/tdrs-backend/tdpservice/users/permissions.py index 4872dfd7f..622b94fec 100644 --- a/tdrs-backend/tdpservice/users/permissions.py +++ b/tdrs-backend/tdpservice/users/permissions.py @@ -2,11 +2,26 @@ from rest_framework import permissions +#TEMP +import logging +logger = logging.getLogger(__name__) + def is_own_stt(request, view): """Verify user belongs to requested STT.""" is_data_prepper = is_in_group(request.user, 'Data Prepper') - requested_stt = view.kwargs.get('stt', request.data.get('stt')) + + # Depending on the request, the STT could be found in three different places + # sp we will merge all together and just do one check + requested_stt = { + **view.kwargs, + **request.data.dict(), + **request.query_params.dict() + }.get('stt') + # TODO: Fix this + logger.info(f'vk: {view.kwargs}, rd: {request.data.dict()}, rq: {request.query_params.dict()}') + logger.info(f'requested_stt: {requested_stt}') + user_stt = request.user.stt_id if hasattr(request.user, 'stt_id') else None return bool( diff --git a/tdrs-frontend/src/actions/reports.js b/tdrs-frontend/src/actions/reports.js index 1e1aa2c2a..b69983c49 100644 --- a/tdrs-frontend/src/actions/reports.js +++ b/tdrs-frontend/src/actions/reports.js @@ -37,7 +37,7 @@ export const clearError = ({ section }) => (dispatch) => { Get a list of files that can be downloaded, mainly used to decide if the download button should be present. */ -export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( +export const getAvailableFileList = ({ quarter = 'Q1', stt, year }) => async ( dispatch ) => { dispatch({ @@ -45,7 +45,7 @@ export const getAvailableFileList = ({ year, quarter = 'Q1' }) => async ( }) try { const response = await axios.get( - `${BACKEND_URL}/reports/?year=${year}&quarter=${quarter}`, + `${BACKEND_URL}/reports/?year=${year}&quarter=${quarter}&stt=${stt.id}`, { responseType: 'json', } diff --git a/tdrs-frontend/src/components/Reports/Reports.jsx b/tdrs-frontend/src/components/Reports/Reports.jsx index 433271d35..64b05c81b 100644 --- a/tdrs-frontend/src/components/Reports/Reports.jsx +++ b/tdrs-frontend/src/components/Reports/Reports.jsx @@ -78,7 +78,11 @@ function Reports() { // Retrieve the files matching the selected year and quarter. dispatch( - getAvailableFileList({ quarter: selectedQuarter, year: selectedYear }) + getAvailableFileList({ + quarter: selectedQuarter, + year: selectedYear, + stt, + }) ) // Update the section header to reflect selections From b927e94facf9f620c9a18c50f0d80c96fbc873ff Mon Sep 17 00:00:00 2001 From: John Willis Date: Tue, 13 Jul 2021 15:10:27 -0400 Subject: [PATCH 6/9] Fix backend tests & linter errors --- tdrs-backend/tdpservice/reports/views.py | 6 +++++- tdrs-backend/tdpservice/users/permissions.py | 20 +++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/tdrs-backend/tdpservice/reports/views.py b/tdrs-backend/tdpservice/reports/views.py index 5dba12a99..0415ac0fa 100644 --- a/tdrs-backend/tdpservice/reports/views.py +++ b/tdrs-backend/tdpservice/reports/views.py @@ -13,16 +13,20 @@ from tdpservice.reports.serializers import ReportFileSerializer from tdpservice.reports.models import ReportFile -from tdpservice.users.permissions import ReportFilePermissions, is_in_group +from tdpservice.users.permissions import ReportFilePermissions logger = logging.getLogger() class ReportFileFilter(filters.FilterSet): """Filters that can be applied to GET requests as query parameters.""" + + # Override the generated definition for the STT field so we can require it. stt = filters.NumberFilter(field_name='stt_id', required=True) class Meta: + """Class metadata linking to the ReportFile and fields accepted.""" + model = ReportFile fields = ['stt', 'quarter', 'year'] diff --git a/tdrs-backend/tdpservice/users/permissions.py b/tdrs-backend/tdpservice/users/permissions.py index 622b94fec..96d7eb3a0 100644 --- a/tdrs-backend/tdpservice/users/permissions.py +++ b/tdrs-backend/tdpservice/users/permissions.py @@ -1,11 +1,8 @@ """Set permissions for users.""" +from collections import ChainMap from rest_framework import permissions -#TEMP -import logging -logger = logging.getLogger(__name__) - def is_own_stt(request, view): """Verify user belongs to requested STT.""" @@ -13,15 +10,12 @@ def is_own_stt(request, view): # Depending on the request, the STT could be found in three different places # sp we will merge all together and just do one check - requested_stt = { - **view.kwargs, - **request.data.dict(), - **request.query_params.dict() - }.get('stt') - # TODO: Fix this - logger.info(f'vk: {view.kwargs}, rd: {request.data.dict()}, rq: {request.query_params.dict()}') - logger.info(f'requested_stt: {requested_stt}') - + request_parameters = ChainMap( + view.kwargs, + request.query_params, + request.data + ) + requested_stt = request_parameters.get('stt') user_stt = request.user.stt_id if hasattr(request.user, 'stt_id') else None return bool( From babe942ec59a7b5018ad1255c2a176f1b5e9829e Mon Sep 17 00:00:00 2001 From: John Willis Date: Tue, 13 Jul 2021 17:06:09 -0400 Subject: [PATCH 7/9] Fixed frontend tests --- tdrs-backend/tdpservice/users/permissions.py | 2 +- tdrs-frontend/src/actions/reports.test.js | 12 +-- .../src/components/FileUpload/FileUpload.jsx | 6 +- .../src/components/Reports/Reports.test.js | 62 +++---------- .../UploadReport/UploadReport.test.js | 14 +-- tdrs-frontend/src/reducers/reports.js | 4 +- tdrs-frontend/src/reducers/reports.test.js | 86 +++++++------------ 7 files changed, 61 insertions(+), 125 deletions(-) diff --git a/tdrs-backend/tdpservice/users/permissions.py b/tdrs-backend/tdpservice/users/permissions.py index 96d7eb3a0..cac18e3d0 100644 --- a/tdrs-backend/tdpservice/users/permissions.py +++ b/tdrs-backend/tdpservice/users/permissions.py @@ -9,7 +9,7 @@ def is_own_stt(request, view): is_data_prepper = is_in_group(request.user, 'Data Prepper') # Depending on the request, the STT could be found in three different places - # sp we will merge all together and just do one check + # so we will merge all together and just do one check request_parameters = ChainMap( view.kwargs, request.query_params, diff --git a/tdrs-frontend/src/actions/reports.test.js b/tdrs-frontend/src/actions/reports.test.js index 23f748e86..7e973ef36 100644 --- a/tdrs-frontend/src/actions/reports.test.js +++ b/tdrs-frontend/src/actions/reports.test.js @@ -40,6 +40,7 @@ describe('actions/reports', () => { expect(actions[0].type).toBe(SET_FILE) expect(actions[0].payload).toStrictEqual({ + file: { name: 'HELLO', type: 'text/plain' }, fileName: 'HELLO', fileType: 'text/plain', section: 'Active Case Data', @@ -57,10 +58,10 @@ describe('actions/reports', () => { const actions = store.getActions() expect(actions[0].type).toBe(SET_FILE_ERROR) - expect(actions[0].payload).toStrictEqual({ - error: Error({ message: 'something went wrong' }), - section: 'Active Case Data', - }) + expect(actions[0].payload).toHaveProperty( + 'error', + TypeError("Cannot read property 'name' of undefined") + ) }) it('should dispatch OPEN_FILE_DIALOG when a file has been successfully downloaded', async () => { @@ -74,7 +75,7 @@ describe('actions/reports', () => { await store.dispatch( download({ - year: 2020, + id: 1, section: 'Active Case Data', }) ) @@ -109,6 +110,7 @@ describe('actions/reports', () => { await store.dispatch( getAvailableFileList({ + stt: { id: 10 }, year: 2020, }) ) diff --git a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx index e843c539c..48ac4647a 100644 --- a/tdrs-frontend/src/components/FileUpload/FileUpload.jsx +++ b/tdrs-frontend/src/components/FileUpload/FileUpload.jsx @@ -2,6 +2,7 @@ import React, { useRef, useEffect } from 'react' import PropTypes from 'prop-types' import { useDispatch, useSelector } from 'react-redux' import fileType from 'file-type/browser' + import { clearError, clearFile, @@ -9,9 +10,7 @@ import { upload, download, } from '../../actions/reports' - import Button from '../Button' - import createFileInputErrorState from '../../utils/createFileInputErrorState' import { handlePreview, getTargetClassName } from './utils' @@ -21,14 +20,13 @@ const INVALID_FILE_ERROR = function FileUpload({ section, setLocalAlertState }) { // e.g. 'Aggregate Case Data' => 'aggregate-case-data' // The set of uploaded files in our Redux state - const { files, quarter, year } = useSelector((state) => state.reports) + const { files } = useSelector((state) => state.reports) const dispatch = useDispatch() // e.g. "1 - Active Case Data" => ["1", "Active Case Data"] const [sectionNumber, sectionName] = section.split(' - ') - console.log({ year, quarter }) const hasFile = files?.some( (file) => file.section === sectionName && file.uuid ) diff --git a/tdrs-frontend/src/components/Reports/Reports.test.js b/tdrs-frontend/src/components/Reports/Reports.test.js index 382d39e21..516e75f7d 100644 --- a/tdrs-frontend/src/components/Reports/Reports.test.js +++ b/tdrs-frontend/src/components/Reports/Reports.test.js @@ -13,6 +13,7 @@ describe('Reports', () => { files: [ { section: 'Active Case Data', + file: null, fileName: null, fileType: null, error: null, @@ -20,6 +21,7 @@ describe('Reports', () => { }, { section: 'Closed Case Data', + file: null, fileName: null, fileType: null, error: null, @@ -27,6 +29,7 @@ describe('Reports', () => { }, { section: 'Aggregate Data', + file: null, fileName: null, fileType: null, error: null, @@ -34,6 +37,7 @@ describe('Reports', () => { }, { section: 'Stratum Data', + file: null, fileName: null, fileType: null, error: null, @@ -301,63 +305,13 @@ describe('Reports', () => { }, }) }) - expect(store.dispatch).toHaveBeenCalledTimes(9) + expect(store.dispatch).toHaveBeenCalledTimes(10) // There should be 4 more dispatches upon making the submission, // one request to /reports for each file fireEvent.click(getByText('Submit Data Files')) await waitFor(() => getByRole('alert')) - expect(store.dispatch).toHaveBeenCalledTimes(13) - }) - - it('should add files to the redux state when uploading', async () => { - // Because mock-redux-store doesn't actually test reducers, - // we need to test this separately from the test above - const store = mockStore(initialState) - - const file1 = makeTestFile('section1.txt') - const file2 = makeTestFile('section2.txt') - const file3 = makeTestFile('section3.txt') - const file4 = makeTestFile('section4.txt') - - await store.dispatch(upload({ file: file1, section: 'Active Case Data' })) - await store.dispatch(upload({ file: file2, section: 'Closed Case Data' })) - await store.dispatch(upload({ file: file3, section: 'Aggregate Data' })) - await store.dispatch(upload({ file: file4, section: 'Stratum Data' })) - - const actions = store.getActions() - - expect(actions[0].type).toBe(SET_FILE) - expect(actions[0].payload).toStrictEqual({ - fileName: 'section1.txt', - fileType: 'text/plain', - section: 'Active Case Data', - uuid: actions[0].payload.uuid, - }) - - expect(actions[1].type).toBe(SET_FILE) - expect(actions[1].payload).toStrictEqual({ - fileName: 'section2.txt', - fileType: 'text/plain', - section: 'Closed Case Data', - uuid: actions[1].payload.uuid, - }) - - expect(actions[2].type).toBe(SET_FILE) - expect(actions[2].payload).toStrictEqual({ - fileName: 'section3.txt', - fileType: 'text/plain', - section: 'Aggregate Data', - uuid: actions[2].payload.uuid, - }) - - expect(actions[3].type).toBe(SET_FILE) - expect(actions[3].payload).toStrictEqual({ - fileName: 'section4.txt', - fileType: 'text/plain', - section: 'Stratum Data', - uuid: actions[3].payload.uuid, - }) + expect(store.dispatch).toHaveBeenCalledTimes(14) }) it('should add files to the redux state when dispatching uploads', async () => { @@ -379,6 +333,7 @@ describe('Reports', () => { expect(actions[0].type).toBe(SET_FILE) expect(actions[0].payload).toStrictEqual({ + file: file1, fileName: 'section1.txt', fileType: 'text/plain', section: 'Active Case Data', @@ -387,6 +342,7 @@ describe('Reports', () => { expect(actions[1].type).toBe(SET_FILE) expect(actions[1].payload).toStrictEqual({ + file: file2, fileName: 'section2.txt', fileType: 'text/plain', section: 'Closed Case Data', @@ -395,6 +351,7 @@ describe('Reports', () => { expect(actions[2].type).toBe(SET_FILE) expect(actions[2].payload).toStrictEqual({ + file: file3, fileName: 'section3.txt', fileType: 'text/plain', section: 'Aggregate Data', @@ -403,6 +360,7 @@ describe('Reports', () => { expect(actions[3].type).toBe(SET_FILE) expect(actions[3].payload).toStrictEqual({ + file: file4, fileName: 'section4.txt', fileType: 'text/plain', section: 'Stratum Data', diff --git a/tdrs-frontend/src/components/UploadReport/UploadReport.test.js b/tdrs-frontend/src/components/UploadReport/UploadReport.test.js index 962bcfe92..a1e87be66 100644 --- a/tdrs-frontend/src/components/UploadReport/UploadReport.test.js +++ b/tdrs-frontend/src/components/UploadReport/UploadReport.test.js @@ -18,11 +18,13 @@ describe('UploadReport', () => { files: [ { fileName: 'test.txt', + id: 1, section: 'Active Case Data', uuid: uuidv4(), }, { fileName: 'testb.txt', + id: 2, section: 'Closed Case Data', uuid: uuidv4(), }, @@ -78,7 +80,7 @@ describe('UploadReport', () => { }, }) - expect(store.dispatch).toHaveBeenCalledTimes(3) + expect(store.dispatch).toHaveBeenCalledTimes(2) expect(container.querySelectorAll('.has-invalid-file').length).toBe(0) }) it('should prevent upload of file with invalid extension', () => { @@ -105,7 +107,7 @@ describe('UploadReport', () => { }, }) - expect(store.dispatch).toHaveBeenCalledTimes(3) + expect(store.dispatch).toHaveBeenCalledTimes(2) expect(container.querySelectorAll('.has-invalid-file').length).toBe(0) }) @@ -116,7 +118,7 @@ describe('UploadReport', () => { const { container } = render( - + ) @@ -131,14 +133,14 @@ describe('UploadReport', () => { const { container } = render( - + ) const buttons = container.querySelectorAll('.tanf-file-download-btn') buttons[0].click() - expect(store.dispatch).toHaveBeenCalledTimes(3) + expect(store.dispatch).toHaveBeenCalledTimes(2) }) it('should render a preview when there is a file available to download', (done) => { @@ -148,7 +150,7 @@ describe('UploadReport', () => { const { container } = render( - + ) setTimeout(() => { diff --git a/tdrs-frontend/src/reducers/reports.js b/tdrs-frontend/src/reducers/reports.js index ad3aeb01e..e6107c3f8 100644 --- a/tdrs-frontend/src/reducers/reports.js +++ b/tdrs-frontend/src/reducers/reports.js @@ -32,7 +32,7 @@ export const getUpdatedFiles = ({ error = null, file = null, }) => { - const oldFileIndex = getFileIndex(state.files, section) + const oldFileIndex = getFileIndex(state?.files, section) const updatedFiles = [...state.files] updatedFiles[oldFileIndex] = { id, @@ -122,7 +122,7 @@ const reports = (state = initialState, action) => { } case SET_SELECTED_YEAR: { const { year } = payload - return { ...state, year: parseInt(year) } + return { ...state, year } } case SET_SELECTED_STT: { const { stt } = payload diff --git a/tdrs-frontend/src/reducers/reports.test.js b/tdrs-frontend/src/reducers/reports.test.js index 070f639a4..ffe477c88 100644 --- a/tdrs-frontend/src/reducers/reports.test.js +++ b/tdrs-frontend/src/reducers/reports.test.js @@ -60,9 +60,13 @@ describe('reducers/reports', () => { payload: { data: [ { - fileName: 'test.txt', + id: 1, + extension: 'txt', + original_filename: 'test.txt', section: 'Active Case Data', - uuid, + quarter: 'Q1', + slug: uuid, + year: 2021, }, ], }, @@ -71,7 +75,11 @@ describe('reducers/reports', () => { files: [ { fileName: 'test.txt', + fileType: 'txt', + id: 1, section: 'Active Case Data', + quarter: 'Q1', + year: 2021, uuid, }, { @@ -108,6 +116,7 @@ describe('reducers/reports', () => { reducer(undefined, { type: SET_FILE, payload: { + file: {}, fileName: 'Test.txt', fileType: 'text/plain', section: 'Stratum Data', @@ -138,10 +147,11 @@ describe('reducers/reports', () => { uuid: null, }, { - data: '', section: 'Stratum Data', + file: {}, fileName: 'Test.txt', fileType: 'text/plain', + id: null, error: null, uuid, }, @@ -184,10 +194,11 @@ describe('reducers/reports', () => { uuid: null, }, { - data: '', section: 'Stratum Data', - fileName: null, + file: null, + fileName: undefined, fileType: null, + id: null, error: null, uuid: null, }, @@ -232,10 +243,11 @@ describe('reducers/reports', () => { uuid: null, }, { - data: '', section: 'Stratum Data', - fileName: null, + file: null, + fileName: undefined, fileType: null, + id: null, error: fakeError, uuid: null, }, @@ -316,10 +328,11 @@ describe('reducers/reports', () => { uuid: null, }, { - data: '', section: 'Stratum Data', + file: null, fileName: null, fileType: null, + id: null, error: null, uuid: null, }, @@ -419,64 +432,27 @@ describe('reducers/reports', () => { }) }) - it('should be able to update files with a new value and return those files', () => { - const updatedFiles = getUpdatedFiles( - initialState, - 'Test.txt', - 'Active Case Data' - ) - - expect(updatedFiles).toStrictEqual([ - { - data: '', - section: 'Active Case Data', - fileName: 'Test.txt', - fileType: null, - error: null, - uuid: null, - }, - { - section: 'Closed Case Data', - fileName: null, - fileType: null, - error: null, - uuid: null, - }, - { - section: 'Aggregate Data', - fileName: null, - fileType: null, - error: null, - uuid: null, - }, - { - section: 'Stratum Data', - fileName: null, - fileType: null, - error: null, - uuid: null, - }, - ]) - }) - it('should be able to update files with a new value and return those files', () => { const uuid = uuidv4() - const updatedFiles = getUpdatedFiles( - initialState, - 'Test.txt', - 'Active Case Data', + const updatedFiles = getUpdatedFiles({ + state: initialState, + id: 10, + file: {}, + fileName: 'Test.txt', + section: 'Active Case Data', + fileType: 'text/plain', uuid, - 'text/plain' - ) + }) expect(updatedFiles).toStrictEqual([ { - data: '', + file: {}, section: 'Active Case Data', fileName: 'Test.txt', fileType: 'text/plain', error: null, + id: 10, uuid, }, { From e02d90809712e94712f891a79b93fdfc50be40e8 Mon Sep 17 00:00:00 2001 From: John Willis Date: Fri, 16 Jul 2021 15:23:13 -0400 Subject: [PATCH 8/9] Add missing AWS setting for django-storages --- tdrs-backend/tdpservice/settings/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index de942006d..2630b9bca 100755 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -121,6 +121,7 @@ class Common(Configuration): AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_BUCKET") AWS_REGION_NAME = os.getenv("AWS_REGION_NAME") + AWS_S3_REGION_NAME = os.getenv("AWS_REGION_NAME") # Those who will receive error notifications from django via email ADMINS = (("Admin1", "ADMIN_EMAIL_FIRST"), ("Admin2", "ADMIN_EMAIL_SECOND")) From 191c6ea900032b08fd2e2ae63e63d7a4d77f6133 Mon Sep 17 00:00:00 2001 From: John Willis Date: Thu, 22 Jul 2021 16:48:22 -0400 Subject: [PATCH 9/9] Fix linter errors --- tdrs-frontend/src/actions/reports.js | 154 +++++++++++++-------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/tdrs-frontend/src/actions/reports.js b/tdrs-frontend/src/actions/reports.js index 8d9fcfa51..4a65fb674 100644 --- a/tdrs-frontend/src/actions/reports.js +++ b/tdrs-frontend/src/actions/reports.js @@ -1,65 +1,65 @@ -import { v4 as uuidv4 } from "uuid"; -import axios from "axios"; +import { v4 as uuidv4 } from 'uuid' +import axios from 'axios' -import axiosInstance from "../axios-instance"; -import { logErrorToServer } from "../utils/eventLogger"; -import removeFileInputErrorState from "../utils/removeFileInputErrorState"; +import axiosInstance from '../axios-instance' +import { logErrorToServer } from '../utils/eventLogger' +import removeFileInputErrorState from '../utils/removeFileInputErrorState' -const BACKEND_URL = process.env.REACT_APP_BACKEND_URL; +const BACKEND_URL = process.env.REACT_APP_BACKEND_URL -export const SET_FILE = "SET_FILE"; -export const CLEAR_FILE = "CLEAR_FILE"; -export const CLEAR_FILE_LIST = "CLEAR_FILE_LIST"; -export const SET_FILE_ERROR = "SET_FILE_ERROR"; -export const CLEAR_ERROR = "CLEAR_ERROR"; +export const SET_FILE = 'SET_FILE' +export const CLEAR_FILE = 'CLEAR_FILE' +export const CLEAR_FILE_LIST = 'CLEAR_FILE_LIST' +export const SET_FILE_ERROR = 'SET_FILE_ERROR' +export const CLEAR_ERROR = 'CLEAR_ERROR' -export const START_FILE_DOWNLOAD = "START_FILE_DOWNLOAD"; -export const FILE_DOWNLOAD_ERROR = "FILE_DOWNLOAD_ERROR"; +export const START_FILE_DOWNLOAD = 'START_FILE_DOWNLOAD' +export const FILE_DOWNLOAD_ERROR = 'FILE_DOWNLOAD_ERROR' -export const FETCH_FILE_LIST = "FETCH_FILE_LIST"; -export const SET_FILE_LIST = "SET_FILE_LIST"; -export const FETCH_FILE_LIST_ERROR = "FETCH_FILE_LIST_ERROR"; -export const DOWNLOAD_DIALOG_OPEN = "DOWNLOAD_DIALOG_OPEN"; +export const FETCH_FILE_LIST = 'FETCH_FILE_LIST' +export const SET_FILE_LIST = 'SET_FILE_LIST' +export const FETCH_FILE_LIST_ERROR = 'FETCH_FILE_LIST_ERROR' +export const DOWNLOAD_DIALOG_OPEN = 'DOWNLOAD_DIALOG_OPEN' export const clearFile = ({ section }) => (dispatch) => { - dispatch({ type: CLEAR_FILE, payload: { section } }); - }; + dispatch({ type: CLEAR_FILE, payload: { section } }) + } export const clearFileList = () => (dispatch) => { - dispatch({ type: CLEAR_FILE_LIST }); -}; + dispatch({ type: CLEAR_FILE_LIST }) +} export const clearError = ({ section }) => (dispatch) => { - dispatch({ type: CLEAR_ERROR, payload: { section } }); - }; + dispatch({ type: CLEAR_ERROR, payload: { section } }) + } /** Get a list of files that can be downloaded, mainly used to decide if the download button should be present. */ export const getAvailableFileList = - ({ quarter = "Q1", stt, year }) => + ({ quarter = 'Q1', stt, year }) => async (dispatch) => { dispatch({ type: FETCH_FILE_LIST, - }); + }) try { const response = await axios.get( `${BACKEND_URL}/reports/?year=${year}&quarter=${quarter}&stt=${stt.id}`, { - responseType: "json", + responseType: 'json', } - ); + ) dispatch({ type: SET_FILE_LIST, payload: { data: response?.data?.results, }, - }); + }) } catch (error) { dispatch({ type: FETCH_FILE_LIST_ERROR, @@ -68,50 +68,50 @@ export const getAvailableFileList = year, quarter, }, - }); + }) } - }; + } export const download = - ({ id, quarter = "Q1", section, year }) => + ({ id, quarter = 'Q1', section, year }) => async (dispatch) => { try { - if (!id) throw new Error("No id was provided to download action."); - dispatch({ type: START_FILE_DOWNLOAD }); + if (!id) throw new Error('No id was provided to download action.') + dispatch({ type: START_FILE_DOWNLOAD }) const response = await axios.get( `${BACKEND_URL}/reports/${id}/download/`, { - responseType: "blob", + responseType: 'blob', } - ); - const data = response.data; + ) + const data = response.data // Create a link and associate it with the blob returned from the file // download - this allows us to trigger the file download dialog without // having to change the route or reload the page. - const url = window.URL.createObjectURL(new Blob([data])); - const link = document.createElement("a"); + const url = window.URL.createObjectURL(new Blob([data])) + const link = document.createElement('a') - link.href = url; - link.setAttribute("download", `${year}.${quarter}.${section}.txt`); + link.href = url + link.setAttribute('download', `${year}.${quarter}.${section}.txt`) - document.body.appendChild(link); + document.body.appendChild(link) // Click the link to actually prompt the file download - link.click(); + link.click() // Cleanup afterwards to prevent unwanted side effects - document.body.removeChild(link); - dispatch({ type: DOWNLOAD_DIALOG_OPEN }); + document.body.removeChild(link) + dispatch({ type: DOWNLOAD_DIALOG_OPEN }) } catch (error) { dispatch({ type: FILE_DOWNLOAD_ERROR, payload: { error, year, quarter, section }, - }); - return false; + }) + return false } - }; + } // Main Redux action to add files to the state export const upload = @@ -127,15 +127,15 @@ export const upload = section, uuid: uuidv4(), }, - }); + }) } catch (error) { - logErrorToServer(SET_FILE_ERROR); - dispatch({ type: SET_FILE_ERROR, payload: { error, section } }); - return false; + logErrorToServer(SET_FILE_ERROR) + dispatch({ type: SET_FILE_ERROR, payload: { error, section } }) + return false } - return true; - }; + return true + } export const submit = ({ @@ -150,7 +150,7 @@ export const submit = }) => async (dispatch) => { const submissionRequests = uploadedFiles.map((file) => { - const formData = new FormData(); + const formData = new FormData() const dataFile = { file: file.file, original_filename: file.fileName, @@ -160,59 +160,59 @@ export const submit = year, stt, quarter, - }; + } for (const [key, value] of Object.entries(dataFile)) { - formData.append(key, value); + formData.append(key, value) } return axiosInstance.post( `${process.env.REACT_APP_BACKEND_URL}/reports/`, formData, { - headers: { "Content-Type": "multipart/form-data" }, + headers: { 'Content-Type': 'multipart/form-data' }, withCredentials: true, } - ); - }); + ) + }) Promise.all(submissionRequests) .then((responses) => { setLocalAlertState({ active: true, - type: "success", + type: 'success', message: `Successfully submitted section(s): ${formattedSections} on ${new Date().toDateString()}`, - }); - removeFileInputErrorState(); + }) + removeFileInputErrorState() const submittedFiles = responses.map( (response) => `${response?.data?.original_filename} (${response?.data?.extension})` - ); + ) // Create LogEntries in Django for each created ReportFile logger.alert( `Submitted ${ submittedFiles.length - } data file(s): ${submittedFiles.join(", ")}`, + } data file(s): ${submittedFiles.join(', ')}`, { files: responses.map((response) => response?.data?.id), - activity: "upload", + activity: 'upload', } - ); + ) }) - .catch((error) => console.error(error)); - }; + .catch((error) => console.error(error)) + } -export const SET_SELECTED_STT = "SET_SELECTED_STT"; -export const SET_SELECTED_YEAR = "SET_SELECTED_YEAR"; -export const SET_SELECTED_QUARTER = "SET_SELECTED_QUARTER"; +export const SET_SELECTED_STT = 'SET_SELECTED_STT' +export const SET_SELECTED_YEAR = 'SET_SELECTED_YEAR' +export const SET_SELECTED_QUARTER = 'SET_SELECTED_QUARTER' export const setStt = (stt) => (dispatch) => { - dispatch({ type: SET_SELECTED_STT, payload: { stt } }); -}; + dispatch({ type: SET_SELECTED_STT, payload: { stt } }) +} export const setYear = (year) => (dispatch) => { - dispatch({ type: SET_SELECTED_YEAR, payload: { year } }); -}; + dispatch({ type: SET_SELECTED_YEAR, payload: { year } }) +} export const setQuarter = (quarter) => (dispatch) => { - dispatch({ type: SET_SELECTED_QUARTER, payload: { quarter } }); -}; + dispatch({ type: SET_SELECTED_QUARTER, payload: { quarter } }) +}