Skip to content

Commit 358caec

Browse files
committed
#238 ( biosecurity) add fq into csv and other csv operations
#265 add download full csv, csv deletion function and admin links to myAnnotation and other small changes
1 parent af8e724 commit 358caec

File tree

10 files changed

+160
-65
lines changed

10 files changed

+160
-65
lines changed

grails-app/assets/stylesheets/alerts.css

+6
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,10 @@ a.btn-ala {
108108
color: #fff; /* White text */
109109
}
110110

111+
.row-fluid {
112+
display: flex;
113+
justify-content: space-between;
114+
align-items: center;
115+
}
116+
111117

grails-app/controllers/au/org/ala/alerts/AdminController.groovy

+24-1
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,9 @@ class AdminController {
731731

732732
// Get a list of all CSV files in the folder
733733
String mergedCSVFile = biosecurityCSVService.aggregateCSVFiles(folderName)
734-
734+
if (folderName == "/" || folderName.isEmpty()) {
735+
folderName = "biosecurity_alerts"
736+
}
735737
def saveToFile = folderName +".csv"
736738
response.contentType = 'application/octet-stream'
737739
response.setHeader('Content-Disposition', "attachment; filename=\"${saveToFile}\"")
@@ -776,4 +778,25 @@ class AdminController {
776778
render(status: 200, text: "QueryResult not found")
777779
}
778780
}
781+
782+
@AlaSecured(value = ['ROLE_ADMIN', 'ROLE_BIOSECURITY_ADMIN'], anyRole = true)
783+
def deleteBiosecurityAuditCSV(String filename) {
784+
Map message = [status : 1, message: ""]
785+
def BASE_DIRECTORY = grailsApplication.config.biosecurity.csv.local.directory
786+
def file = new File(BASE_DIRECTORY, filename)
787+
if (!file.exists() || file.isDirectory()) {
788+
message['status'] = 1
789+
message['message'] = "File not found"
790+
} else {
791+
if (file.renameTo(new File(BASE_DIRECTORY, filename +'_deleted'))){
792+
message['status'] = 0
793+
message['message'] = "The file has been temporarily deleted. You can contact the system administrator to recover it."
794+
} else {
795+
message['status'] = 1
796+
message['message'] = "File deletion failed"
797+
}
798+
}
799+
800+
render(status: 200, contentType: 'application/json', text: message as JSON)
801+
}
779802
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class UrlMappings {
3131
"/admin/deleteQuery"(controller: 'admin', action: 'deleteQuery')
3232
"/biosecurity/csv"(controller: 'admin', action: 'listBiosecurityAuditCSV')
3333
"/biosecurity/csv/download"(controller: 'admin', action: 'downloadBiosecurityAuditCSV')
34+
"/biosecurity/csv/delete"(controller: 'admin', action: 'deleteBiosecurityAuditCSV')
3435
"/biosecurity/csv/aggregate"(controller: 'admin', action: 'aggregateBiosecurityAuditCSV')
3536

3637

grails-app/services/au/org/ala/alerts/BiosecurityCSVService.groovy

+56-39
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,15 @@ class BiosecurityCSVService {
7575

7676
def tempFilePath = Files.createTempFile(outputFile, ".csv")
7777
def tempFile = tempFilePath.toFile()
78-
String rawHeader = "recordID:uuid, recordLink:occurrenceLink, scientificName,taxonConceptID,decimalLatitude,decimalLongitude,eventDate,occurrenceStatus,dataResourceName,multimedia,media_id:image," +
78+
// example of rawHeader
79+
// recordID:uuid recordID is the header name, uuid is the property in the record
80+
String rawHeader = "recordID:uuid, recordLink:occurrenceLink, scientificName,taxonConceptID,decimalLatitude,decimalLongitude,eventDate,occurrenceStatus,dataResourceName,multimedia,mediaId:image," +
7981
"vernacularName,taxonConceptID_new,kingdom,phylum,class:classs,order,family,genus,species,subspecies," +
8082
"firstLoadedDate:firstLoaded,basisOfRecord,match," +
81-
"search_term,correct_name:scientificName,provided_name:providedName,common_name:vernacularName,state:stateProvince,lga,shape,list_id:listId,list_name:listName,cw_state,shape_feature,creator:collector," +
82-
"license,mimetype,width,height," +
83-
"image_url:smallImageUrl," + // TBC , multiple image urls
84-
"date_sent:dateSent,"+
85-
"cl"
83+
"searchTerm:search_term,correct name:scientificName,provided name:providedName,common name:vernacularName,state:stateProvince,lga layer,lga,fq,list id:listId,list name:listName, listLink:listLink, cw_state,shape feature:shape_feature,creator:collector," +
84+
"license,mimetype," +
85+
"image url:smallImageUrl," + // TBC , multiple image urls
86+
"date sent:dateSent"
8687
//"fq, kvs"
8788
if (grailsApplication.config.biosecurity.csv.headers) {
8889
rawHeader = grailsApplication.config.biosecurity.csv.headers
@@ -92,7 +93,6 @@ class BiosecurityCSVService {
9293
def fields = []
9394
def headersAndFields = rawHeader.split(',')
9495
headersAndFields.each { entry ->
95-
9696
def parts = entry.trim().split(':', 2) // Split on ':' with a limit of 2 parts
9797
headers << parts[0] // Add the part before ':' to the first array
9898
if (parts.size() > 1) {
@@ -107,36 +107,14 @@ class BiosecurityCSVService {
107107
writer.write(headers.join(",")+ "\n")
108108
records.each { record ->
109109
def values = fields.collect { field ->
110-
def value = ""
111-
if(record.containsKey(field)) {
112-
value = record[field]
113-
//if value is a list, convert it to a string. e.g. collectors, images
114-
if (value instanceof List) {
115-
value = "\"${value.join(";")}\"" // Join the list with ';' and wrap it in double quotes
116-
} else {
117-
value = value.toString()
118-
switch (field) {
119-
case "eventDate":
120-
if (value) {
121-
value = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss").format(value.toLong())
122-
}
123-
break
124-
default:
125-
if (value instanceof List) {
126-
value = "\"${value.join(";")}\"" // Join the list with ';' and wrap it in double quotes
127-
} else {
128-
value = value.toString()
129-
}
130-
break
131-
}
132-
}
133-
} else {
134-
//special cases
135-
if(field == "lga") {
110+
def value = record[field]
111+
112+
switch (field) {
113+
case "lga":
136114
//read from cl (context layer)
137115
def cls = record["cl"]
138-
//LGA2023
139-
def layerId= grailsApplication.config.biosecurity.csv.lga ?:"LGA2023"
116+
//LGA2023 is the default layer id
117+
def layerId = grailsApplication.config.getProperty('biosecurity.csv.lga', 'LGA2023')
140118
if(cls) {
141119
String matched = cls.find {
142120
def (k, v) = it.split(':') // Split the string into key and value
@@ -145,8 +123,30 @@ class BiosecurityCSVService {
145123
//assure return "" if matched is null
146124
value = matched?.split(':')?.with { it.size() > 1 ? it[1] : "" } ?: ""
147125
}
148-
}
126+
break
127+
case "lga layer":
128+
value = grailsApplication.config.biosecurity.csv.lga ?:"LGA2023"
129+
break
130+
case "eventDate":
131+
if (value) {
132+
value = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss").format(value.toLong())
133+
} else {
134+
value = ""
135+
}
136+
break
137+
default:
138+
if (record.containsKey(field)) {
139+
if (value instanceof List) {
140+
value = "\"${value.join(";")}\"" // Join the list with ';' and wrap it in double quotes
141+
} else {
142+
value = value.toString()
143+
}
144+
} else {
145+
value = ""
146+
}
147+
break
149148
}
149+
150150
return value
151151
}
152152
writer.write(values.join(","))
@@ -165,9 +165,10 @@ class BiosecurityCSVService {
165165
String aggregateCSVFiles(String folderName) {
166166
def BASE_DIRECTORY = grailsApplication.config.biosecurity.csv.local.directory
167167
def folder = new File(BASE_DIRECTORY, folderName)
168-
Collection<File> csvFiles = folder.listFiles().findAll { it.isFile() && it.name.endsWith('.csv') }
168+
Collection<File> csvFiles = []
169+
collectCsvFiles(folder, csvFiles)
169170

170-
log.info("Aggregate CSV files into one file")
171+
log.info("Aggregate ${csvFiles.size()} CSV files under ${folder} into one file")
171172
def tempFilePath = Files.createTempFile("merged_", ".csv")
172173
def tempFile = tempFilePath.toFile()
173174
tempFile.withWriter { writer ->
@@ -189,6 +190,21 @@ class BiosecurityCSVService {
189190
return tempFile.absolutePath
190191
}
191192

193+
/**
194+
* Collect CSV files from the folder and its subfolders
195+
* @param folder
196+
* @param collectedFiles
197+
*/
198+
private void collectCsvFiles(File folder, Collection<File> collectedFiles) {
199+
folder.listFiles().each { file ->
200+
if (file.isDirectory()) {
201+
collectCsvFiles(file, collectedFiles) // Recursively collect CSV files from subfolders
202+
} else if (file.isFile() && file.name.endsWith('.csv')) {
203+
collectedFiles.add(file)
204+
}
205+
}
206+
}
207+
192208
/**
193209
* Move file from source to destination
194210
* @param source
@@ -219,13 +235,14 @@ class BiosecurityCSVService {
219235
* @param dir
220236
* @return Key value pair of folder and files
221237
* */
238+
222239
private List<Map> listFilesRecursively(File dir) {
223240
def BASE_DIRECTORY = grailsApplication.config.biosecurity.csv.local.directory
224241
def rootDir = new File(BASE_DIRECTORY)
225242
def foldersAndFiles = rootDir.listFiles().findAll { it.isDirectory() }.collect { folder ->
226243
[
227244
name: folder.name,
228-
files: folder.listFiles().findAll { it.isFile() }.collect { file -> file.name }
245+
files: folder.listFiles().findAll { it.isFile() && it.name.endsWith('.csv') }.collect { file -> file.name }
229246
]
230247
}
231248
return foldersAndFiles

grails-app/services/au/org/ala/alerts/BiosecurityService.groovy

+2
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ class BiosecurityService {
181181
record["dateSent"] = new SimpleDateFormat("dd/MM/yyyy").format(to)
182182
record["listName"] = query.name
183183
record["listId"] = drId
184+
record["listLink"] = grailsApplication.config.getProperty('lists.baseURL') + "/speciesListItem/list/" + drId
184185
return record
185186
}
186187

@@ -245,6 +246,7 @@ class BiosecurityService {
245246
if (listItem.kvpValues?.size()>0) {
246247
//Do not join, let CSV generate handle it
247248
occurrence['kvs'] = listItem.kvpValues.collect { kv -> "${kv.key}:${kv.value}" }
249+
occurrence['fq'] = listItem.kvpValues?.find { it.key == 'fq' }?.value
248250
}
249251
fetchExtraInfo(occurrence.uuid, occurrence)
250252
}

grails-app/services/au/org/ala/alerts/EmailService.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class EmailService {
106106
int totalRecords = records.size()
107107
int maxRecords = grailsApplication.config.getProperty("biosecurity.query.maxRecords", Integer, 500)
108108

109-
if (queryResult.hasChanged || Environment.current == Environment.DEVELOPMENT || Environment.current == Environment.TEST) {
109+
if (queryResult.hasChanged || Environment.current == Environment.DEVELOPMENT ) {
110110
if (grailsApplication.config.getProperty("mail.enabled", Boolean, false)) {
111111
def emails = recipients.collect { it.email }
112112
log.info "Sending emails for ${query.name} to ${emails.size() <= 2 ? emails.join('; ') : emails.take(2).join('; ') + ' and ' + emails.size() + ' other users.'}"

grails-app/services/au/org/ala/alerts/UserService.groovy

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import grails.converters.JSON
2020
import grails.plugin.cache.Cacheable
2121
import grails.util.Holders
2222
import grails.util.Environment
23-
import au.org.ala.alerts.Query
23+
2424

2525
import grails.gorm.transactions.Transactional
2626

@@ -36,7 +36,6 @@ class UserService {
3636
def getUserAlertsConfig(User user) {
3737

3838
log.debug('getUserAlertsConfig - Viewing my alerts : ' + user)
39-
4039
//enabled alerts
4140
def notificationInstanceList = Notification.findAllByUser(user)
4241

grails-app/views/admin/biosecurity.gsp

+4-4
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,13 @@
397397
</div>
398398
<p></p>
399399
<div class="row" style="text-align: right">
400-
<div class="col-sm-10" >CSV files generated for each Biosecurity Alert [Experimental purpose]</div>
400+
<div class="col-sm-10" >Download CSV list of all occurrences from all biosecurity alerts sent (scheduled and manual)</div>
401401
<div class="col-sm-2" >
402-
<a class="btn btn-info" href="${createLink(controller: 'admin', action: 'listBiosecurityAuditCSV')}" target="_blank">CSV Auditing</a>
402+
<a class="btn btn-info" href="${createLink(controller: 'admin', action: 'listBiosecurityAuditCSV')}" target="_blank">Reporting</a>
403403
</div>
404404
</div>
405405
<p></p>
406-
<div>
406+
%{-- <div>
407407
<g:if test="${queries}">
408408
<form target="_blank" action="${request.contextPath}/admin/csvAllBiosecurity" method="post">
409409
<div class="row" style="text-align: right">
@@ -416,7 +416,7 @@
416416
</div>
417417
</form>
418418
</g:if>
419-
</div>
419+
</div>--}%
420420

421421
</div>
422422
</div>

grails-app/views/admin/biosecurityCSV.gsp

+51-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<title>Biosecurity Audit CSV </title>
4+
<title>Biosecurity Alerts Reporting</title>
55
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
66
<meta name="layout" content="${grailsApplication.config.skin.layout}"/>
77
<meta name="breadcrumb" content="BioSecurity CSV"/>
@@ -30,23 +30,60 @@
3030
icon.toggleClass('fa-folder fa-folder-open-o');
3131
});
3232
});
33+
34+
function deleteFile(filename) {
35+
$.ajax({
36+
url: "${createLink(controller: 'admin', action: 'deleteBiosecurityAuditCSV')}",
37+
type: 'POST',
38+
data: {
39+
filename: filename
40+
},
41+
success: function(response) {
42+
// Assuming the response is a JSON object with a message
43+
alert(response.message);
44+
location.reload();
45+
},
46+
error: function(xhr, status, error) {
47+
alert("Error: " + xhr.responseText);
48+
}
49+
});
50+
}
3351
</script>
3452
</head>
3553
<body>
54+
<div>
55+
<h2>All Biosecurity Alerts Data</h2>
56+
Download a comprehensive CSV file detailing all occurrence records from every biosecurity alert sent. This includes both scheduled and manually triggered emails
57+
<br/>
58+
<a class="btn btn-primary " href="${createLink(controller: 'admin', action: 'aggregateBiosecurityAuditCSV', params: [folderName:'/'])}">
59+
<i class="fa fa-cloud-download" aria-hidden="true" ></i> Download Full CSV Report
60+
</a>
61+
<hr>
62+
</div>
63+
3664
<g:if test="${status == 0}">
37-
<g:each in="${foldersAndFiles}" var="folder">
38-
<div class="folder" data-folder="${folder.name}">
39-
<i class="fa fa-folder folder-icon folder" aria-hidden="true"></i> ${folder.name}
40-
<a href="${createLink(controller: 'admin', action: 'aggregateBiosecurityAuditCSV', params: [folderName:folder.name])}">
41-
<i class="fa fa-cloud-download" aria-hidden="true" title="Download as one CSV file for the date."></i>
42-
</a>
43-
</div>
44-
<div class="file-list" id="files-${folder.name}">
45-
<g:each in="${folder.files}" var="file">
46-
<div><a href="${createLink(controller: 'admin', action: 'downloadBiosecurityAuditCSV', params: [filename:folder.name +'/' + file])}"><i class="fa fa-download" aria-hidden="true"></i> ${file}</a></div>
47-
</g:each>
48-
</div>
49-
</g:each>
65+
<div>
66+
<h2>Individual Biosecurity Alerts Data</h2>
67+
Download individual CSV files for each biosecurity alert email, detailing all occurrence records. Files are sorted by the date the alert was sent.
68+
<g:each in="${foldersAndFiles}" var="folder">
69+
<div class="folder" data-folder="${folder.name}">
70+
<i class="fa fa-folder folder-icon folder" aria-hidden="true"></i> ${folder.name}
71+
<a href="${createLink(controller: 'admin', action: 'aggregateBiosecurityAuditCSV', params: [folderName:folder.name])}">
72+
<i class="fa fa-cloud-download" aria-hidden="true" title="Download as one CSV file for the date."></i>
73+
</a>
74+
</div>
75+
<div class="file-list" id="files-${folder.name}">
76+
<g:each in="${folder.files}" var="file">
77+
<div>
78+
<a href="${createLink(controller: 'admin', action: 'downloadBiosecurityAuditCSV', params: [filename:folder.name +'/' + file])}"><i class="fa fa-download" aria-hidden="true"></i> ${file}</a>
79+
<a href="#" onclick="deleteFile('${folder.name}/${file}'); return false;">
80+
<i class="fa fa-trash-o" aria-hidden="true"></i>
81+
</a>
82+
</div>
83+
</g:each>
84+
</div>
85+
</g:each>
86+
</div>
5087
</g:if>
5188
<g:else>
5289
${message}

grails-app/views/notification/myAlerts.gsp

+14-4
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,25 @@
1111
<meta name="breadcrumbParent" content="${grailsApplication.config.userDetails.web.url}/myprofile, ${message(code:"my.alerts.breadcrumb.parent")}" />
1212
<g:set var="userPrefix" value="${adminUser ? user.email : message(code:"my.alerts.my") }"/>
1313
<title><g:message code="my.alerts.title" args="[userPrefix]" /> | ${grailsApplication.config.skin.orgNameLong}</title>
14-
1514
<asset:stylesheet href="alerts.css"/>
1615
</head>
1716
<body>
1817
<div id="content">
1918
<header id="page-header">
20-
<div class="inner row-fluid">
21-
<h1><g:message code="my.alerts.h1" args="[userPrefix]" /></h1>
22-
</div>
19+
<div class="inner row-fluid">
20+
<div class="content">
21+
<h1><g:message code="my.alerts.h1" args="[userPrefix]" /></h1>
22+
</div>
23+
<div>
24+
<% if (request.isUserInRole("ROLE_ADMIN")) { %>
25+
<a href="${createLink(controller: 'admin', action: 'index')}" class="btn btn-primary">Admin</a>
26+
<% } %>
27+
28+
<% if (request.isUserInRole("ROLE_BIOSECURITY_ADMIN")) { %>
29+
<a href="${createLink(controller: 'admin', action: 'biosecurity')}" class="btn btn-primary">Biosecurity Admin</a>
30+
<% } %>
31+
</div>
32+
</div>
2333
</header>
2434
<g:if test="${flash.message}">
2535
<div class="alert alert-info">${flash.message}</div>

0 commit comments

Comments
 (0)