Skip to content

Commit 59c31e0

Browse files
authored
Merge pull request #3460 from AtlasOfLivingAustralia/feature/issue3421
Feature/issue3421
2 parents 63895ee + 65d704c commit 59c31e0

File tree

25 files changed

+649
-49
lines changed

25 files changed

+649
-49
lines changed

.github/workflows/build.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
steps:
2121
- uses: actions/checkout@v3
2222

23-
- name: Set up JDK 11
23+
- name: Set up JDK 17
2424
uses: actions/setup-java@v3
2525
with:
26-
java-version: '11'
26+
java-version: '17'
2727
distribution: 'adopt'
2828

2929
- name: Install nodejs

build.gradle

+14-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ plugins {
2121
id 'jacoco'
2222
id 'com.williamhill.wiremock' version '0.4.1'
2323
}
24+
sourceCompatibility = '11'
25+
targetCompatibility = '17'
2426

2527
version "$meritVersion"
2628
group "au.org.ala"
@@ -152,6 +154,17 @@ dependencies {
152154
implementation "org.commonmark:commonmark:0.24.0"
153155
implementation "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1"
154156

157+
// Used to get a token that can be used to access the BDR API hosted in Azure
158+
implementation platform('com.azure:azure-sdk-bom:1.2.28')
159+
160+
implementation 'com.azure:azure-identity'
161+
implementation 'com.azure:azure-storage-blob'
162+
163+
// Used to access the Cognito Identity Pool as tokens from the user pool
164+
// don't have an audience claim which is required by Azure
165+
implementation(platform("software.amazon.awssdk:bom:2.27.21"))
166+
implementation 'software.amazon.awssdk:cognitoidentity'
167+
155168
compileOnly "io.micronaut:micronaut-inject-groovy"
156169
console "org.grails:grails-console"
157170
profile "org.grails.profiles:web"
@@ -188,7 +201,7 @@ dependencies {
188201
providedCompile "io.methvin:directory-watcher:0.4.0"
189202

190203
if (!Boolean.valueOf(inplace)) {
191-
implementation "org.grails.plugins:ecodata-client-plugin:7.3-SNAPSHOT"
204+
implementation "org.grails.plugins:ecodata-client-plugin:8.0-SNAPSHOT"
192205
}
193206
}
194207

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx2048M
1616
seleniumVersion=3.12.0
1717
seleniumSafariDriverVersion=3.14.0
1818
snapshotCacheTime=1800
19-
alaSecurityLibsVersion=6.3.0
19+
alaSecurityLibsVersion=7.0.0

grails-app/assets/javascripts/dataSets.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ var DataSetsViewModel =function(dataSets, projectService, config) {
1818
return new DataSetSummary(dataSet);
1919
});
2020

21+
self.downloadProjectDataSetsUrl = config.downloadProjectDataSetsUrl;
22+
2123
self.newDataSet = function() {
2224
window.location.href = config.newDataSetUrl;
2325
};
@@ -40,6 +42,30 @@ var DataSetsViewModel =function(dataSets, projectService, config) {
4042
return (report && report.name);
4143
}
4244

45+
function isDownloadableMonitorDataSet(dataSet) {
46+
47+
if (dataSet.collectionApp !== MONITOR_APP) {
48+
return false;
49+
}
50+
var protocolId = dataSet.protocol;
51+
var downloadableProtocols = config.downloadableProtocols || [];
52+
53+
var isDownloadable = downloadableProtocols.indexOf(protocolId) >= 0;
54+
if (isDownloadable) {
55+
var now = moment();
56+
var creationDate = moment(dataSet.dateCreated);
57+
var minutesToInjestDataSet = config.minutesToInjestDataSet || 1;
58+
if (dataSet.progress !== ActivityProgress.planned) {
59+
if (creationDate.add(minutesToInjestDataSet, 'minutes').isBefore(now)) {
60+
isDownloadable = true;
61+
}
62+
}
63+
}
64+
return isDownloadable;
65+
}
66+
67+
self.enableProjectDataSetsDownload = _.find(dataSets, isDownloadableMonitorDataSet) != null;
68+
4369
/** View model backing for a single row in the data set summary table */
4470
function DataSetSummary(dataSet) {
4571

@@ -69,20 +95,24 @@ var DataSetsViewModel =function(dataSets, projectService, config) {
6995
});
7096
};
7197

98+
this.downloadUrl = null;
99+
if (isDownloadableMonitorDataSet(dataSet)) {
100+
this.downloadUrl = config.downloadDataSetUrl + '/' + dataSet.dataSetId;
101+
}
102+
72103
if (this.createdIn === MONITOR_APP) {
73104
if (this.progress == ActivityProgress.planned) {
74105
var now = moment();
75106
var creationDate = moment(dataSet.dateCreated);
107+
76108
if (creationDate.add(1, 'minutes').isBefore(now)) {
77109
this.progress = 'sync error';
78110
}
79111
else {
80112
this.progress = 'sync in progress';
81113
}
82-
83114
}
84115
}
85-
86116
}
87117
};
88118

grails-app/assets/javascripts/projects.js

+3
Original file line numberDiff line numberDiff line change
@@ -1271,8 +1271,11 @@ function ProjectPageViewModel(project, sites, activities, userRoles, config) {
12711271
editDataSetUrl: config.editDataSetUrl,
12721272
deleteDataSetUrl: config.deleteDataSetUrl,
12731273
viewDataSetUrl: config.viewDataSetUrl,
1274+
downloadDataSetUrl: config.downloadDataSetUrl,
1275+
downloadProjectDataSetsUrl: config.downloadProjectDataSetsUrl,
12741276
returnToUrl: config.returnToUrl,
12751277
reports: project.reports || [],
1278+
downloadableProtocols: config.downloadableProtocols,
12761279
viewReportUrl: config.viewReportUrl
12771280
};
12781281
var projectService = new ProjectService({}, config);

grails-app/conf/application.groovy

+14
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,17 @@ security {
251251
requiredClaims = ["sub", "iat", "exp", "jti", "client_id"]
252252
}
253253
}
254+
bdr.api.url="https://changeMe.org.au/api"
255+
bdr.api.readTimeout=60000
256+
bdr['client-id']="changeMe"
257+
bdr['client-secret']="changeMe"
258+
bdr.discoveryUri="https://changeMe.org.au/.well-known"
259+
bdr.jwtScopes="read"
260+
bdr.azure.clientId='changeMe'
261+
bdr.azure.tenantId='changeMe'
262+
bdr.azure.apiScope='api://changeme/.default'
263+
//bdr.dataSet.formats=["application/geo+json","application/rdf+xml", "text/turtle", "application/ld+json", "application/n-triples"]
264+
bdr.dataSet.formats=["application/geo+json", "text/turtle"]
254265

255266
webservice.jwt = true
256267
webservice['jwt-scopes'] = "ala/internal users/read ala/attrs ecodata/read_test ecodata/write_test"
@@ -302,6 +313,8 @@ environments {
302313
app.default.hub='merit'
303314
wiremock.port = 8018
304315
security.oidc.discoveryUri = "http://localhost:${wiremock.port}/cas/oidc/.well-known"
316+
security.jwt.discoveryUri = "http://localhost:${wiremock.port}/cas/oidc/.well-known"
317+
bdr.discoveryUri = "http://localhost:${wiremock.port}/cas/oidc/.well-known"
305318
security.oidc.allowUnsignedIdTokens = true
306319
security.oidc.clientId="oidcId"
307320
security.oidc.secret="oidcSecret"
@@ -333,6 +346,7 @@ environments {
333346
spatial.baseUrl = "http://localhost:${wiremock.port}"
334347
spatial.layersUrl = spatial.baseUrl + "/ws"
335348
grails.mail.port = 3025 // com.icegreen.greenmail.util.ServerSetupTest.SMTP.port
349+
336350
}
337351
production {
338352
grails.logging.jul.usebridge = false

grails-app/conf/logback.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<appender-ref ref="STDOUT" />
1616
</logger>
1717

18-
<logger name="org.pac4j" level="WARN">
18+
<logger name="org.pac4j" level="INFO">
1919
<appender-ref ref="STDOUT" />
2020
</logger>
2121

grails-app/conf/spring/resources.groovy

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import au.org.ala.merit.CheckRisksAndThreatsTask
22
import au.org.ala.merit.MeritServletContextConfig
3+
import au.org.ala.merit.config.BdrTokenConfig
4+
import au.org.ala.merit.hub.HubAwareLinkGenerator
35
import au.org.ala.merit.StatisticsFactory
46
import au.org.ala.merit.hub.HubAwareLinkGenerator
57
import au.org.ala.merit.reports.NHTOutputReportLifecycleListener
@@ -27,4 +29,5 @@ beans = {
2729
RegionalCapacityServicesReport(RegionalCapacityServicesReportLifecycleListener)
2830

2931
meritServletContextConfig(MeritServletContextConfig)
32+
bdrTokenConfig(BdrTokenConfig)
3033
}

grails-app/controllers/au/org/ala/merit/DataSetController.groovy

+62-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package au.org.ala.merit
22

3-
import au.org.ala.merit.PreAuthorise
4-
import au.org.ala.merit.ProjectService
3+
54
import au.org.ala.merit.config.ProgramConfig
65
import grails.converters.JSON
76
import org.springframework.http.HttpStatus
87

98
class DataSetController {
109

1110
static allowedMethods = [create:'GET', edit:'GET', save:'POST', delete:'POST']
12-
11+
private static final Integer DEFAULT_BDR_QUERY_LIMIT = 5000
1312
ProjectService projectService
1413
DataSetSummaryService dataSetSummaryService
14+
BdrService bdrService
15+
WebService webService
1516

1617
// Note that authorization is done against a project, so the project id must be supplied to the method.
1718
@PreAuthorise(accessLevel = 'editor')
@@ -157,6 +158,64 @@ class DataSetController {
157158
render response as JSON
158159
}
159160

161+
@PreAuthorise(accessLevel = 'admin')
162+
def downloadProjectDataSets(String id, String format, Integer limit) {
163+
if (!id) {
164+
render status: HttpStatus.NOT_FOUND
165+
return
166+
}
167+
Map projectData = projectData(id)
168+
List supportedFormats = grailsApplication.config.getProperty('bdr.dataSet.formats', List)
169+
if (!format) {
170+
format = supportedFormats[0]
171+
}
172+
if (format !in supportedFormats) {
173+
render status: HttpStatus.BAD_REQUEST
174+
return
175+
}
176+
177+
bdrService.downloadProjectDataSet(id, format, projectData.project.name, response, limit ?: DEFAULT_BDR_QUERY_LIMIT)
178+
}
179+
180+
@PreAuthorise(accessLevel = 'admin')
181+
def download(String id, String dataSetId, String format, Integer limit) {
182+
Map projectData = projectData(id)
183+
184+
List supportedFormats = grailsApplication.config.getProperty('bdr.dataSet.formats', List)
185+
if (!format) {
186+
format = supportedFormats[0]
187+
}
188+
if (format !in supportedFormats) {
189+
render status: HttpStatus.BAD_REQUEST
190+
return
191+
}
192+
193+
Map dataSet = projectData.project?.custom?.dataSets?.find{it.dataSetId == dataSetId}
194+
195+
if (!dataSet) {
196+
render status: HttpStatus.NOT_FOUND
197+
return
198+
}
199+
else {
200+
if (isMonitorDataSet(dataSet)) {
201+
if (isProtocolSupportedForDownload(dataSet)) {
202+
bdrService.downloadDataSet(id, dataSet.dataSetId, dataSet.name, format, response, limit ?: DEFAULT_BDR_QUERY_LIMIT)
203+
}
204+
}
205+
else if (dataSet.url) {
206+
webService.proxyGetRequest(response, dataSet.url, false)
207+
}
208+
}
209+
}
210+
211+
private static boolean isMonitorDataSet(Map dataSet) {
212+
return dataSet.protocol
213+
}
214+
215+
private static boolean isProtocolSupportedForDownload(Map dataSet) {
216+
return true
217+
}
218+
160219
@PreAuthorise(accessLevel = 'editor')
161220
def delete(String id) {
162221

grails-app/controllers/au/org/ala/merit/ProjectController.groovy

+14-1
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,16 @@ class ProjectController {
200200
if (datasetsVisible && project.custom?.dataSets) {
201201
projectService.filterDataSetSummaries(project.custom?.dataSets)
202202
}
203+
List downloadableProtocols = downloadableProtocols()
204+
203205
boolean showExternalIds = userService.userHasReadOnlyAccess() || userService.userIsSiteAdmin()
204206
def model = [overview : [label: 'Overview', visible: true, default: true, type: 'tab', publicImages: imagesModel, displayOutcomes: false, blog: blog, hasNewsAndEvents: hasNewsAndEvents, hasProjectStories: hasProjectStories, canChangeProjectDates: canChangeProjectDates, outcomes:project.outcomes, objectives:config.program?.config?.objectives, showExternalIds:showExternalIds],
205207
documents : [label: 'Documents', visible: config.includesContent(ProgramConfig.ProjectContent.DOCUMENTS), type: 'tab', user:user, template:'docs', activityPeriodDescriptor:config.activityPeriodDescriptor ?: 'Stage'],
206208
details : [label: 'MERI Plan', default: false, disabled: !meriPlanEnabled, visible: meriPlanVisible, meriPlanVisibleToUser: meriPlanVisibleToUser, risksAndThreatsVisible: canViewRisks, announcementsVisible: true, project:project, type: 'tab', template:'viewMeriPlan', meriPlanTemplate:MERI_PLAN_TEMPLATE+'View', config:config, activityPeriodDescriptor:config.activityPeriodDescriptor ?: 'Stage'],
207209
plan : [label: 'Activities', visible: true, disabled: !user?.hasViewAccess, type: 'tab', template:'projectActivities', grantManagerSettingsVisible:user?.isCaseManager, project:project, reports: project.reports, scores: scores, risksAndThreatsVisible: risksAndThreatsVisible],
208210
site : [label: 'Sites', visible: config.includesContent(ProgramConfig.ProjectContent.SITES), disabled: !user?.hasViewAccess, editable:user?.isEditor, type: 'tab', template:'projectSites'],
209211
dashboard : [label: 'Dashboard', visible: config.includesContent(ProgramConfig.ProjectContent.DASHBOARD), disabled: !user?.hasViewAccess, type: 'tab'],
210-
datasets : [label: 'Data set summary', visible: datasetsVisible, template: '/project/dataset/dataSets', type:'tab'],
212+
datasets : [label: 'Data set summary', visible: datasetsVisible, template: '/project/dataset/dataSets', downloadableProtocols: downloadableProtocols, supportedFormats:bdrDataSetSupportedFormats(), type:'tab'],
211213
admin : [label: 'Admin', visible: adminTabVisible, user:user, type: 'tab', template:'projectAdmin', project:project, canChangeProjectDates: canChangeProjectDates, minimumProjectEndDate:minimumProjectEndDate, showMERIActivityWarning:true, showAnnouncementsTab: showAnnouncementsTab, showSpecies:true, meriPlanTemplate:MERI_PLAN_TEMPLATE, showMeriPlanHistory:showMeriPlanHistory, requireMeriPlanApprovalReason:Boolean.valueOf(config.supportsMeriPlanHistory), config:config, activityPeriodDescriptor:config.activityPeriodDescriptor ?: 'Stage', canRegenerateReports: canRegenerateReports, hasSubmittedOrApprovedFinalReportInCategory: hasSubmittedOrApprovedFinalReportInCategory, canModifyMeriPlan: canModifyMeriPlan, showRequestLabels:config.supportsParatoo, outcomeStartIndex:outcomeStartIndex]]
212214

213215
if (template == MERI_ONLY_TEMPLATE) {
@@ -274,6 +276,17 @@ class ProjectController {
274276
return [view: 'index', model: model]
275277
}
276278

279+
private List<String> downloadableProtocols() {
280+
String BDR_DOWNLOAD_SUPPORTED_TAG = 'bdr_download_supported'
281+
List<Map> forms = activityService.monitoringProtocolForms()
282+
forms = forms.findAll{BDR_DOWNLOAD_SUPPORTED_TAG in it.tags}
283+
forms.collect{it.externalId }
284+
}
285+
286+
private List bdrDataSetSupportedFormats() {
287+
grailsApplication.config.getProperty("bdr.dataSet.formats", List.class)
288+
}
289+
277290
private Map buildRLPTargetsModel(Map model, project){
278291
//Verify project.outcomes (from program config) with primaryOutcome and secondaryOutcomes in project.custom.details.outcomes
279292
Map primaryOutcome = project.custom?.details?.outcomes?.primaryOutcome

grails-app/controllers/au/org/ala/merit/UrlMappings.groovy

+5
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ class UrlMappings {
128128
action = 'upload'
129129
}
130130

131+
"/dataSet/download/$id/$dataSetId" {
132+
controller = 'dataSet'
133+
action = 'download'
134+
}
135+
131136
"500"(view:'/error')
132137
"404"(view:'/404')
133138
"/$hub/$controller/ws/$action/$id" {

grails-app/i18n/messages.properties

+4-1
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,7 @@ report.status.published=Report approved
368368
report.status.pendingApproval=Report submitted
369369
report.status.unpublished=Report not submitted
370370
report.status.cancelled=Report not required
371-
report.status.=
371+
report.status.=
372+
373+
bdr.dataSet.format.application/geo+json=GeoJSON
374+
bdr.dataSet.format.text/turtle=Turtle

0 commit comments

Comments
 (0)