Skip to content

Commit

Permalink
Surveys list export (#3431)
Browse files Browse the repository at this point in the history
* added CSV export button

* added server side part

* include owner email

* changed exported file name

* code cleanup

* format dates

---------

Co-authored-by: Stefano Ricci <SteRiccio@users.noreply.github.com>
  • Loading branch information
SteRiccio and SteRiccio authored May 22, 2024
1 parent 8cf1b4f commit df98a4b
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 14 deletions.
1 change: 1 addition & 0 deletions core/auth/authorizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const getMaxSurveysUserCanCreate = (user) => {
export const canViewSurvey = (user, surveyInfo) =>
User.isSystemAdmin(user) || _hasAuthGroupForSurvey({ user, surveyInfo })
export const canExportSurvey = _hasSurveyPermission(permissions.surveyEdit)
export const canExportSurveysList = (user) => User.isSystemAdmin(user)
export const canViewTemplates = (user) => User.isSystemAdmin(user)

// UPDATE
Expand Down
1 change: 1 addition & 0 deletions server/modules/auth/authApiMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const requireSurveyViewPermission = requireSurveyPermission(Authorizer.ca
export const requireSurveyEditPermission = requireSurveyPermission(Authorizer.canEditSurvey)
export const requireRecordCleansePermission = requireSurveyPermission(Authorizer.canCleanseRecords)
export const requireSurveyRdbRefreshPermission = requirePermission(Authorizer.canRefreshAllSurveyRdbs)
export const requireCanExportSurveysList = requirePermission(Authorizer.canExportSurveysList)

// Record
export const requireRecordListViewPermission = requireSurveyPermission(Authorizer.canViewSurvey)
Expand Down
16 changes: 15 additions & 1 deletion server/modules/survey/api/surveyApi.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as R from 'ramda'

import { Authorizer } from '@openforis/arena-core'
import * as DateUtils from '@core/dateUtils'
import * as FileUtils from '@server/utils/file/fileUtils'
import * as ProcessUtils from '@core/processUtils'
Expand All @@ -17,7 +18,6 @@ import * as SurveyService from '../service/surveyService'
import * as FileService from '../../record/service/fileService'
import * as UserService from '../../user/service/userService'
import { ExportFileNameGenerator } from '@server/utils/exportFileNameGenerator'
import { Authorizer } from '@openforis/arena-core'

export const init = (app) => {
// ==== CREATE
Expand Down Expand Up @@ -110,6 +110,20 @@ export const init = (app) => {
}
})

app.get('/surveys/export', AuthMiddleware.requireCanExportSurveysList, async (req, res, next) => {
try {
const user = Request.getUser(req)
const { draft = true, template = false } = Request.getParams(req)
const date = DateUtils.nowFormatDefault()
const fileName = `arena_surveys_${date}.csv`
Response.setContentTypeFile({ res, fileName, contentType: Response.contentTypes.csv })

await SurveyService.exportSurveysList({ user, draft, template, outputStream: res })
} catch (error) {
next(error)
}
})

app.get('/surveyTemplates', async (req, res, next) => {
try {
const user = Request.getUser(req)
Expand Down
15 changes: 14 additions & 1 deletion server/modules/survey/manager/surveyManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,19 @@ export const fetchSurveyAndNodeDefsAndRefDataBySurveyId = async (
}

export const fetchUserSurveysInfo = async (
{ user, draft = true, template = false, offset, limit, lang, search, sortBy, sortOrder, includeCounts = false },
{
user,
draft = true,
template = false,
offset,
limit,
lang,
search,
sortBy,
sortOrder,
includeCounts = false,
includeOwnerEmailAddress = false,
},
client = db
) => {
// check sortBy is valid
Expand All @@ -308,6 +320,7 @@ export const fetchUserSurveysInfo = async (
search,
sortBy,
sortOrder,
includeOwnerEmailAddress,
})
).map(assocSurveyInfo)

Expand Down
17 changes: 16 additions & 1 deletion server/modules/survey/repository/surveyRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const _getSurveysSelectQuery = ({
limit = null,
sortBy = Survey.sortableKeys.dateModified,
sortOrder = 'DESC',
includeOwnerEmailAddress = false,
}) => {
const checkAccess = (!template || draft) && !User.isSystemAdmin(user)
const propsCol = draft ? '(s.props || s.props_draft)' : 's.props'
Expand Down Expand Up @@ -120,6 +121,7 @@ const _getSurveysSelectQuery = ({
NULL
END AS status,
u.name AS owner_name
${includeOwnerEmailAddress ? `, u.email AS owner_email` : ''}
${checkAccess ? ', json_build_array(row_to_json(g.*)) AS auth_groups' : ''}
FROM survey s
JOIN "user" u
Expand All @@ -143,6 +145,7 @@ const _getSurveysSelectQuery = ({
, s.status
, ${labelCol} AS label
, s.owner_name
${includeOwnerEmailAddress ? `, s.owner_email AS owner_email` : ''}
${checkAccess ? ', s.auth_groups' : ''}
FROM survey_view s
WHERE
Expand All @@ -166,13 +169,25 @@ export const fetchUserSurveys = async (
search: searchParam = null,
sortBy = Survey.sortableKeys.dateModified,
sortOrder = 'DESC',
includeOwnerEmailAddress = false,
},
client = db
) => {
const search = StringUtils.isNotBlank(searchParam) ? `%${searchParam.toLowerCase()}%` : null

return client.map(
_getSurveysSelectQuery({ user, draft, template, offset, limit, lang, search, sortBy, sortOrder }),
_getSurveysSelectQuery({
user,
draft,
template,
offset,
limit,
lang,
search,
sortBy,
sortOrder,
includeOwnerEmailAddress,
}),
{ userUuid: User.getUuid(user), template, search },
(def) => DB.transformCallback(def, true)
)
Expand Down
56 changes: 53 additions & 3 deletions server/modules/survey/service/surveyService.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as JobManager from '@server/job/jobManager'
import * as JobUtils from '@server/job/jobUtils'
import * as SurveyManager from '../manager/surveyManager'
import * as DateUtils from '@core/dateUtils'

import { RecordsUpdateThreadService } from '@server/modules/record/service/update/surveyRecordsThreadService'
import * as JobManager from '@server/job/jobManager'
import * as JobUtils from '@server/job/jobUtils'
import * as CSVWriter from '@server/utils/file/csvWriter'

import * as SurveyManager from '../manager/surveyManager'
import SurveyCloneJob from './clone/surveyCloneJob'
import SurveyExportJob from './surveyExport/surveyExportJob'
import SurveyPublishJob from './publish/surveyPublishJob'
Expand Down Expand Up @@ -42,6 +44,54 @@ export const exportSurvey = ({ surveyId, user, includeData = false, includeActiv
return { job: JobUtils.jobToJSON(job), outputFileName }
}

export const exportSurveysList = async ({ user, draft, template, outputStream }) => {
const items = await fetchUserSurveysInfo({
user,
draft,
template,
includeCounts: true,
includeOwnerEmailAddress: true,
})
const fields = [
'id',
'uuid',
'name',
'label',
'status',
'dateCreated',
'dateModified',
'datePublished',
'ownerName',
'ownerEmail',
'nodeDefsCount',
'recordsCount',
'chainsCount',
'filesCount',
'filesSize',
'filesMissing',
]

const objectTransformer = (surveySummary) =>
Object.entries(surveySummary).reduce((acc, [key, value]) => {
const valueTransformed = key.startsWith('date')
? DateUtils.convertDate({
dateStr: value,
formatFrom: DateUtils.formats.datetimeISO,
formatTo: DateUtils.formats.datetimeExport,
})
: value
acc[key] = valueTransformed
return acc
}, {})

await CSVWriter.writeItemsToStream({
outputStream,
items,
fields,
options: { objectTransformer, removeNewLines: false },
})
}

export const cloneSurvey = ({ user, surveyId, surveyInfoTarget, cycle = null }) => {
const job = new SurveyCloneJob({ user, surveyId, surveyInfoTarget, cycle })
JobManager.executeJobThread(job)
Expand Down
23 changes: 15 additions & 8 deletions webapp/components/survey/Surveys/HeaderLeft/HeaderLeft.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@ import React from 'react'
import PropTypes from 'prop-types'

import { TextInput } from '@webapp/components/form'
import { ButtonDownload } from '@webapp/components/buttons'
import { useAuthCanExportSurveysList } from '@webapp/store/user/hooks'

const HeaderLeft = (props) => {
const { handleSearch, search, totalCount } = props

const canExportSurveys = useAuthCanExportSurveysList()

if (!totalCount) return null

return (
<TextInput
className="surveys__header-left__input-search"
defaultValue={search}
onChange={(val) => {
handleSearch(val)
}}
placeholder="surveysView.filterPlaceholder"
/>
<>
<TextInput
className="surveys__header-left__input-search"
defaultValue={search}
onChange={(val) => {
handleSearch(val)
}}
placeholder="surveysView.filterPlaceholder"
/>
{canExportSurveys && <ButtonDownload href="/api/surveys/export" label="common.csvExport" />}
</>
)
}

Expand Down
1 change: 1 addition & 0 deletions webapp/store/user/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const useAuthCanExportSurvey = () => Authorizer.canExportSurvey(useUser()
export const useAuthCanViewTemplates = () => Authorizer.canViewTemplates(useUser())
export const useAuthCanCreateTemplate = () => Authorizer.canCreateTemplate(useUser())
export const useAuthCanEditTemplates = () => Authorizer.canEditTemplates(useUser())
export const useAuthCanExportSurveysList = () => Authorizer.canExportSurveysList(useUser())

// ====== Auth / Analysis
export const useAuthCanUseAnalysis = () => Authorizer.canAnalyzeRecords(useUser(), useSurveyInfo())
Expand Down

0 comments on commit df98a4b

Please sign in to comment.