Skip to content

Commit 7cf834d

Browse files
authored
Merge pull request #341 from AtlasOfLivingAustralia/259_email_template_design
259 email template design
2 parents e7797c8 + 5f5a648 commit 7cf834d

31 files changed

+1839
-581
lines changed

grails-app/conf/application.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ userdetails:
179179
useSpeciesListsAlerts: true
180180
useSpatialAlerts: true
181181
useBlogsAlerts: true
182-
useCitizenScienceAlerts: true
182+
useCitizenScienceAlerts: false
183+
183184
biosecurity:
184185
cronExpression: '0 0 11 ? * THU'
185186
query:

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

+21
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,27 @@ class AdminController {
741741
}
742742
}
743743

744+
/**
745+
* Reset the previous / current results stored in QueryResult
746+
* @return
747+
*/
748+
@AlaSecured(value = ['ROLE_ADMIN'], anyRole = true)
749+
@Transactional
750+
def resetQueryResult() {
751+
try{
752+
def id = params.id.toInteger()
753+
if(id){
754+
queryResultService.reset(id)
755+
render([status: 0, message: "Query result has been reset"] as JSON)
756+
} else {
757+
render([status: 1, message: "Missing ID"] as JSON)
758+
}
759+
} catch (Exception e) {
760+
render([status: 1, message: "Error in resetting query result: ${e.message}"] as JSON)
761+
}
762+
}
763+
764+
744765
@AlaSecured(value = ['ROLE_ADMIN', 'ROLE_BIOSECURITY_ADMIN'], anyRole = true, redirectController = 'notification', redirectAction = 'myAlerts', message = "You don't have permission to view that page.")
745766
def listBiosecurityAuditCSV() {
746767
def result = [:]

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

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import static io.swagger.v3.oas.annotations.enums.ParameterIn.HEADER
3939
class WebserviceController {
4040

4141
def queryService
42+
def queryResultService
4243
def userService
4344
def notificationService
4445
def biosecurityService
@@ -364,6 +365,7 @@ class WebserviceController {
364365
redirectIfSupplied(params)
365366
}
366367

368+
367369
@Operation(
368370
method = "POST",
369371
tags = "alerts",

grails-app/domain/au/org/ala/alerts/QueryResult.groovy

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package au.org.ala.alerts
22

3-
import com.jayway.jsonpath.JsonPath
4-
53
import java.text.SimpleDateFormat
64
import java.util.zip.GZIPInputStream
75
import java.util.zip.GZIPOutputStream
@@ -75,7 +73,7 @@ class QueryResult {
7573
}
7674

7775
String toString() {
78-
"${frequency?.name}: ${lastChanged ? 'Changed' : 'No Change'} on ${lastChecked}"
76+
"${frequency?.name}: ${query?.name} : ${id} ${lastChanged ? 'Changed' : 'No Change'} on ${lastChecked}"
7977
}
8078

8179
Map brief() {
@@ -129,7 +127,6 @@ class QueryResult {
129127
}
130128

131129
byte[] compress(String json) {
132-
//store the last result from the webservice call
133130
if (json) {
134131
ByteArrayOutputStream bout = new ByteArrayOutputStream()
135132
GZIPOutputStream gzout = new GZIPOutputStream(bout)

grails-app/init/au/org/ala/alerts/BootStrap.groovy

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import ala.postie.BiosecurityQueriesJob
44

55
class BootStrap {
66

7-
javax.sql.DataSource dataSource
87
def grailsApplication
98
def messageSource
109
def siteLocale
@@ -119,7 +118,7 @@ class BootStrap {
119118

120119
title = messageSource.getMessage("query.citizen.records.title", null, siteLocale)
121120
descr = messageSource.getMessage("query.citizen.records.descr", null, siteLocale)
122-
if (grailsApplication.config.useCitizenScienceAlerts.toBoolean() &&
121+
if (grailsApplication.config.useCitizenScienceAlerts?.toBoolean() &&
123122
Query.findAllByName(title).isEmpty()) {
124123
Query newCitizenScienceRecords = (new Query([
125124
baseUrl: grailsApplication.config.biocacheService.baseURL,
@@ -142,7 +141,7 @@ class BootStrap {
142141

143142
title = messageSource.getMessage("query.citizen.records.imgs.title", null, siteLocale)
144143
descr = messageSource.getMessage("query.citizen.records.imgs.descr", null, siteLocale)
145-
if (grailsApplication.config.useCitizenScienceAlerts.toBoolean() &&
144+
if (grailsApplication.config.useCitizenScienceAlerts?.toBoolean() &&
146145
Query.findAllByName(title).isEmpty()) {
147146
Query newCitizenScienceRecordsWithImages = (new Query([
148147
baseUrl: grailsApplication.config.biocacheService.baseURL,

grails-app/jobs/ala/postie/HourlyQueriesJob.groovy

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package ala.postie
22

3+
import grails.plugins.schwartz.monitor.listener.QuartzJobListener
4+
import org.quartz.JobExecutionContext
5+
36
class HourlyQueriesJob {
47

58
static triggers = {
@@ -11,6 +14,6 @@ class HourlyQueriesJob {
1114
def execute() {
1215
log.info("****** Starting hourly update ****** " + new Date())
1316
notificationService.execQueryForFrequency('hourly')
14-
log.info("****** Finished hourly update ******" + new Date())
17+
log.info("****** Finished hourly update ****** " + new Date())
1518
}
1619
}

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

+7-9
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ package au.org.ala.alerts
1717

1818
import com.jayway.jsonpath.JsonPath
1919
import com.jayway.jsonpath.PathNotFoundException
20-
import grails.converters.JSON
2120
import org.grails.web.json.JSONArray
2221
import org.grails.web.json.JSONObject
23-
22+
import groovy.json.JsonOutput
2423
import java.text.SimpleDateFormat
2524

2625
/**
@@ -67,11 +66,10 @@ class AnnotationService {
6766
}
6867

6968
/**
70-
* If fireNotZero property is true, diff method will not be called
69+
* If fireNotZero property is true, this method will not be called
7170
*
72-
* for normal alerts, comparing occurrence uuid is enough to show the difference.
73-
* for my annotation alerts, same occurrence record could exist in both result but have different assertions.
74-
* so comparing occurrence uuid is not enough, we need to compare 50001/50002/50003 sections inside each occurrence record
71+
* This method compare the difference of Annotations between previous and last result.
72+
* for annotation query, it returns the records that have new annotations after the given DATE. So it does not require to call this method.
7573
7674
* return a list of records that their annotations have been changed or deleted
7775
* @param String previous
@@ -99,9 +97,9 @@ class AnnotationService {
9997
def record = it.value
10098
def previousRecord = oldRecordsMap.get(record.uuid)
10199
if (previousRecord) {
102-
String currentAssertions = JSON.stringify(filterAssertions(record.user_assertions))
103-
String previousAssertions = JSON.stringify(filterAssertions(previousRecord.user_assertions))
104-
currentAssertions || previousAssertions
100+
String currentAssertions = JsonOutput.toJson(record.user_assertions)
101+
String previousAssertions = JsonOutput.toJson(previousRecord.user_assertions)
102+
currentAssertions != previousAssertions
105103
} else {
106104
true
107105
}

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

+17-15
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,23 @@ class DiffService {
6868
// The following check is determined by the last propertyValue, since it overwrites the previous one
6969

7070
queryResult.propertyValues.each { pv ->
71-
log.debug("[QUERY " + queryResult.query.id + "] " +
72-
" Has changed check:" + pv.propertyPath.name
73-
+ ", value:" + pv.currentValue
74-
+ ", previous:" + pv.previousValue
75-
+ ", fireWhenNotZero:" + pv.propertyPath.fireWhenNotZero
76-
+ ", fireWhenChange:" + pv.propertyPath.fireWhenChange
77-
)
78-
79-
// Two different types of queries: Biocache and Blog/News
80-
// Biocache: totalRecords and last_loaded_records
81-
// Blog/News: last_blog_id
82-
if (pv.propertyPath.fireWhenNotZero) {
83-
changed = pv.currentValue?.toInteger() ?: 0 > 0
84-
} else if (pv.propertyPath.fireWhenChange) {
85-
changed = pv.previousValue != pv.currentValue
71+
if (pv) {
72+
log.debug("[QUERY " + queryResult.query.id + "] " +
73+
" Has changed check:" + pv.propertyPath.name
74+
+ ", value:" + pv.currentValue
75+
+ ", previous:" + pv.previousValue
76+
+ ", fireWhenNotZero:" + pv.propertyPath.fireWhenNotZero
77+
+ ", fireWhenChange:" + pv.propertyPath.fireWhenChange
78+
)
79+
80+
// Two different types of queries: Biocache and Blog/News
81+
// Biocache: totalRecords and last_loaded_records
82+
// Blog/News: last_blog_id
83+
if (pv.propertyPath.fireWhenNotZero) {
84+
changed = pv.currentValue?.toInteger() ?: 0 > 0
85+
} else if (pv?.propertyPath.fireWhenChange) {
86+
changed = pv.previousValue != pv.currentValue
87+
}
8688
}
8789
}
8890

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ class ImageService {
3333
def dataResourceInfos = collectionService.findResourceByUids(collectionUrl, drIds)
3434
drIds.each { id ->
3535
def dataResourceInfo = dataResourceInfos[id]
36-
groupedByDataResource[id].each { record ->
37-
record["dataResourceInfo"] = [:]
38-
record["dataResourceInfo"].lastUpdated = dataResourceInfo.lastUpdated
39-
record["dataResourceInfo"].alaPublicUrl = dataResourceInfo.alaPublicUrl
36+
if (dataResourceInfo) {
37+
groupedByDataResource[id].each { record ->
38+
record["dataResourceInfo"] = [:]
39+
record["dataResourceInfo"].lastUpdated = dataResourceInfo.lastUpdated
40+
record["dataResourceInfo"].alaPublicUrl = dataResourceInfo.alaPublicUrl
41+
}
4042
}
4143
}
4244
return groupedByDataResource

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

+39-46
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,26 @@ class MyAnnotationService{
4646
}catch (PathNotFoundException e){
4747
log.warn("Current result is empty or doesn't have any records containing a field ${recordJsonPath} defined in recordJsonPath")
4848
}
49-
// if an occurrence record doesn't exist in previous result (added) or has different open_assertions or verified_assertions or corrected_assertions than previous (changed).
49+
// if an occurrence record doesn't exist in previous result (added) or has different verified_assertions than previous (changed).
5050
def records = curRecordsMap.findAll {
5151
def record = it.value
52-
!oldRecordsMap.containsKey(record.uuid) ||
53-
record.open_assertions != oldRecordsMap.get(record.uuid).open_assertions ||
54-
record.verified_assertions != oldRecordsMap.get(record.uuid).verified_assertions ||
55-
record.corrected_assertions != oldRecordsMap.get(record.uuid).corrected_assertions
56-
}.values()
52+
// if the record has verified assertions and it is a list - some legacy records may have a String instead of a list
53+
if (!record.verified_assertions?.isEmpty() && record.verified_assertions instanceof List) {
54+
if (oldRecordsMap.containsKey(record.uuid)) {
55+
if (oldRecordsMap.get(record.uuid).verified_assertions?.isEmpty()) {
56+
return true
57+
} else {
58+
return record.verified_assertions.collect { it.uuid }.join(',') != oldRecordsMap.get(record.uuid).verified_assertions.collect { it.uuid }.join(',')
59+
}
60+
} else {
61+
return true
62+
}
63+
} else {
64+
//No verified assertions in current records
65+
return false
66+
}
5767

68+
}.values()
5869

5970
//if an occurrence record exists in previous result but not in current, it means the annotation is deleted.
6071
//We need to add these records as a 'modified' record
@@ -88,28 +99,14 @@ class MyAnnotationService{
8899
JSONArray assertions = assertionsData.json as JSONArray
89100
occurrence.put('user_assertions', assertions)
90101

91-
def (origUserAssertions, openAssertions, verifiedAssertions, correctedAssertions) = filterMyAssertions(assertions, userId)
92-
// only include record has at least 1 (50001/50002/50003) assertion
102+
def verifiedAssertions = findVerifiedAssertions(assertions, userId)
103+
// only include record has at least 1 (50001/50002/50003) assertions that VERIFY the user's assertions
93104
// They will be used for diffService (records that will be included in alert email)
94-
if (!openAssertions.isEmpty() || !verifiedAssertions.isEmpty() || !correctedAssertions.isEmpty()) {
95-
96-
// find the open/verfied/corrected annotations which COMMENTed on all the assertions created by the users
97-
def processedAssertionIds = openAssertions.collect { it.uuid } + verifiedAssertions.collect { it.uuid } + correctedAssertions.collect { it.uuid }
98-
def processedAssertions = assertions.findAll{
99-
processedAssertionIds.contains(it.relatedUuid)
100-
}
101-
occurrence.put('processed_assertions', processedAssertions)
102-
103-
// Those open/verified/corrected assertions will be used to retrieve diff (records that will be included in alert email)
104-
openAssertions.sort { it.uuid }
105-
verifiedAssertions.sort { it.uuid }
106-
correctedAssertions.sort { it.uuid }
107-
occurrence.put('open_assertions', openAssertions.collect { it.uuid }.join(','))
108-
occurrence.put('verified_assertions', verifiedAssertions.collect { it.uuid }.join(','))
109-
occurrence.put('corrected_assertions', correctedAssertions.collect { it.uuid }.join(','))
105+
if ( !verifiedAssertions.isEmpty()) {
106+
occurrence.put('verified_assertions', verifiedAssertions)
107+
reconstructedOccurrences.push(occurrence)
110108
}
111109
}
112-
reconstructedOccurrences.push(occurrence)
113110
}
114111
}
115112
reconstructedOccurrences.sort { it.uuid }
@@ -123,34 +120,30 @@ class MyAnnotationService{
123120

124121
/**
125122
* Search the assertions which the USER has made
126-
* return those have been open-issued, verified or corrected
123+
* return assertions which verify or comment the user's assertions
127124
* @param assertions
128125
* @param userId
129126
* @return
130127
*/
131-
132-
private static def filterMyAssertions(JSONArray assertions, String userId) {
133-
def origUserAssertions = []
134-
def openAssertions = []
135-
def verifiedAssertions = []
136-
def correctedAssertions = []
128+
private static def findVerifiedAssertions(JSONArray assertions, String userId) {
129+
def sortedAssertions = []
137130
if (assertions) {
138131
// all the original user assertions (issues users flagged)
139-
origUserAssertions = assertions.findAll { it.uuid && !it.relatedUuid && it.userId == userId }
140-
141-
// all the 50001 (open issue) assertions (could belong to userId or other users)
142-
def openIssueIds = assertions.findAll { it.uuid && it.relatedUuid && it.code == 50000 && it.qaStatus == 50001 }.collect { it.relatedUuid }
143-
144-
// all the 50002 (verified) assertions (could belong to userId or other users)
145-
def verifiedIds = assertions.findAll { it.uuid && it.relatedUuid && it.code == 50000 && it.qaStatus == 50002 }.collect { it.relatedUuid }
146-
147-
// all the 50003 (corrected) assertions (could belong to userId or other users)
148-
def correctedIds = assertions.findAll { it.uuid && it.relatedUuid && it.code == 50000 && it.qaStatus == 50003 }.collect { it.relatedUuid }
132+
def origUserAssertions = assertions.findAll { it.uuid && !it.relatedUuid && it.userId == userId }
133+
// Find assertions which commented on the original user assertions
134+
def myOriginalUuids = origUserAssertions*.uuid
135+
def verifiedAssertions = myOriginalUuids.collectMany { uuid ->
136+
assertions.findAll { it.relatedUuid == uuid && ( it.qaStatus == 50001 || it.qaStatus == 50002 || it.qaStatus == 50003) }
137+
}
149138

150-
openAssertions = origUserAssertions.findAll { openIssueIds.contains(it.uuid) }
151-
verifiedAssertions = origUserAssertions.findAll { verifiedIds.contains(it.uuid) }
152-
correctedAssertions = origUserAssertions.findAll { correctedIds.contains(it.uuid) }
139+
try {
140+
sortedAssertions = verifiedAssertions.sort { a, b ->
141+
Date.parse("yyyy-MM-dd'T'HH:mm:ssX", b.created) <=> Date.parse("yyyy-MM-dd'T'HH:mm:ssX", a.created)
142+
}
143+
} catch (Exception e) {
144+
sortedAssertions = verifiedAssertions
145+
}
153146
}
154-
return [origUserAssertions, openAssertions, verifiedAssertions, correctedAssertions,]
147+
return sortedAssertions
155148
}
156149
}

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

+11-9
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package au.org.ala.alerts
33
import com.jayway.jsonpath.JsonPath
44
import grails.gorm.transactions.NotTransactional
55
import grails.converters.JSON
6-
import org.apache.commons.io.IOUtils
76
import org.apache.commons.lang.time.DateUtils
87
import org.grails.web.json.JSONArray
9-
import org.grails.web.json.JSONElement
108
import org.grails.web.json.JSONObject
119

1210
import javax.transaction.Transactional
@@ -164,12 +162,17 @@ class NotificationService {
164162
def duration = TimeCategory.minus(endTime, startTime)
165163
qr.addLog("Time cost: ${duration}")
166164
if(!dryRun) {
167-
QueryResult.withTransaction {
168-
if (!qr.save(validate: true, flush: true)) {
169-
qr.errors.allErrors.each {
170-
log.error(it)
165+
try {
166+
QueryResult.withTransaction {
167+
if (!qr.save(validate: true)) {
168+
qr.errors.allErrors.each {
169+
log.error(it)
170+
}
171171
}
172172
}
173+
} catch (Exception e) {
174+
//todo check why sometimes scheduler cannot save the queryresult
175+
log.error("An unexpected error occurred in saving queryresult: ${qr}", e)
173176
}
174177
}
175178
else {
@@ -646,10 +649,9 @@ class NotificationService {
646649
log.debug("Checking frequency : " + frequencyName)
647650
Date now = new Date()
648651
Frequency frequency = Frequency.findByName(frequencyName)
649-
execQueryForFrequency(frequency, sendEmails)
650-
//update the frequency last checked
651-
frequency = Frequency.findByName(frequencyName)
652652
if (frequency) {
653+
execQueryForFrequency(frequency, sendEmails)
654+
//update the frequency last checked
653655
frequency.lastChecked = now
654656
Frequency.withTransaction {
655657
if (!frequency.save(validate: true, flush: true)) {

0 commit comments

Comments
 (0)