• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DataBiosphere / consent / #6242

29 Jul 2025 07:41PM UTC coverage: 83.19% (+0.09%) from 83.102%
#6242

push

web-flow
[DT-1982] RADAR email triggers (#2622)

Co-authored-by: rushtong <grushton@broadinstitute.org>
Co-authored-by: Gregory Rushton <rushtong@users.noreply.github.com>
Co-authored-by: Rachel Johanek <55256690+rjohanek@users.noreply.github.com>
Co-authored-by: rjohanek <rjohanek@broadinstitute.org>
Co-authored-by: Matt Bemis <MatthewBemis@users.noreply.github.com>

291 of 328 new or added lines in 12 files covered. (88.72%)

1 existing line in 1 file now uncovered.

10848 of 13040 relevant lines covered (83.19%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.22
/src/main/java/org/broadinstitute/consent/http/service/VoteService.java
1
package org.broadinstitute.consent.http.service;
2

3
import static java.util.function.Predicate.not;
4

5
import com.google.api.client.http.HttpStatusCodes;
6
import com.google.common.annotations.VisibleForTesting;
7
import com.google.gson.Gson;
8
import com.google.gson.reflect.TypeToken;
9
import com.google.inject.Inject;
10
import freemarker.template.TemplateException;
11
import java.io.IOException;
12
import java.lang.reflect.Type;
13
import java.sql.SQLException;
14
import java.util.ArrayList;
15
import java.util.Collections;
16
import java.util.HashMap;
17
import java.util.HashSet;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Objects;
21
import java.util.Set;
22
import java.util.stream.Collectors;
23
import org.apache.commons.validator.routines.EmailValidator;
24
import org.broadinstitute.consent.http.db.DacDAO;
25
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
26
import org.broadinstitute.consent.http.db.DatasetDAO;
27
import org.broadinstitute.consent.http.db.ElectionDAO;
28
import org.broadinstitute.consent.http.db.UserDAO;
29
import org.broadinstitute.consent.http.db.VoteDAO;
30
import org.broadinstitute.consent.http.enumeration.DataUseTranslationType;
31
import org.broadinstitute.consent.http.enumeration.ElectionStatus;
32
import org.broadinstitute.consent.http.enumeration.ElectionType;
33
import org.broadinstitute.consent.http.enumeration.UserRoles;
34
import org.broadinstitute.consent.http.enumeration.VoteType;
35
import org.broadinstitute.consent.http.exceptions.ConsentConflictException;
36
import org.broadinstitute.consent.http.models.Dac;
37
import org.broadinstitute.consent.http.models.DataAccessRequest;
38
import org.broadinstitute.consent.http.models.Dataset;
39
import org.broadinstitute.consent.http.models.Election;
40
import org.broadinstitute.consent.http.models.Study;
41
import org.broadinstitute.consent.http.models.StudyProperty;
42
import org.broadinstitute.consent.http.models.User;
43
import org.broadinstitute.consent.http.models.Vote;
44
import org.broadinstitute.consent.http.models.dataset_registration_v1.builder.DatasetRegistrationSchemaV1Builder;
45
import org.broadinstitute.consent.http.models.dto.DatasetMailDTO;
46
import org.broadinstitute.consent.http.service.dao.VoteServiceDAO;
47
import org.broadinstitute.consent.http.util.ComplianceLogger;
48
import org.broadinstitute.consent.http.util.ConsentLogger;
49
import org.broadinstitute.consent.http.util.gson.GsonUtil;
50
import org.glassfish.jersey.server.ContainerRequest;
51

52
public class VoteService implements ConsentLogger {
53

54
  private final UserDAO userDAO;
55
  private final DacDAO dacDAO;
56
  private final DataAccessRequestDAO dataAccessRequestDAO;
57
  private final DatasetDAO datasetDAO;
58
  private final ElectionDAO electionDAO;
59
  private final EmailService emailService;
60
  private final ElasticSearchService elasticSearchService;
61
  private final UseRestrictionConverter useRestrictionConverter;
62
  private final VoteDAO voteDAO;
63
  private final VoteServiceDAO voteServiceDAO;
64

65
  @Inject
66
  public VoteService(
67
      UserDAO userDAO,
68
      DacDAO dacDAO,
69
      DataAccessRequestDAO dataAccessRequestDAO,
70
      DatasetDAO datasetDAO,
71
      ElectionDAO electionDAO,
72
      EmailService emailService,
73
      ElasticSearchService elasticSearchService,
74
      UseRestrictionConverter useRestrictionConverter,
75
      VoteDAO voteDAO,
76
      VoteServiceDAO voteServiceDAO) {
1✔
77
    this.userDAO = userDAO;
1✔
78
    this.dacDAO = dacDAO;
1✔
79
    this.dataAccessRequestDAO = dataAccessRequestDAO;
1✔
80
    this.datasetDAO = datasetDAO;
1✔
81
    this.electionDAO = electionDAO;
1✔
82
    this.emailService = emailService;
1✔
83
    this.elasticSearchService = elasticSearchService;
1✔
84
    this.useRestrictionConverter = useRestrictionConverter;
1✔
85
    this.voteDAO = voteDAO;
1✔
86
    this.voteServiceDAO = voteServiceDAO;
1✔
87
  }
1✔
88

89
  /**
90
   * Create votes for an election
91
   *
92
   * @param election The Election
93
   * @param electionType The Election type
94
   * @param isManualReview Is this a manual review election
95
   * @return List of votes
96
   */
97
  @SuppressWarnings("DuplicatedCode")
98
  public List<Vote> createVotes(
99
      Election election, ElectionType electionType, Boolean isManualReview) {
100
    Dac dac = electionDAO.findDacForElection(election.getElectionId());
1✔
101
    Set<User> users;
102
    if (dac != null) {
1✔
103
      users = userDAO.findUsersEnabledToVoteByDAC(dac.getDacId());
×
104
    } else {
105
      users = userDAO.findNonDacUsersEnabledToVote();
1✔
106
    }
107
    List<Vote> votes = new ArrayList<>();
1✔
108
    if (users != null) {
1✔
109
      for (User user : users) {
1✔
110
        votes.addAll(createVotesForUser(user, election, electionType, isManualReview));
1✔
111
      }
1✔
112
    }
113
    return votes;
1✔
114
  }
115

116
  /**
117
   * Create election votes for a user
118
   *
119
   * @param user DACUser
120
   * @param election Election
121
   * @param electionType ElectionType
122
   * @param isManualReview Is election manual review
123
   * @return List of created votes
124
   */
125
  public List<Vote> createVotesForUser(
126
      User user, Election election, ElectionType electionType, Boolean isManualReview) {
127
    Dac dac = electionDAO.findDacForElection(election.getElectionId());
1✔
128
    List<Vote> votes = new ArrayList<>();
1✔
129
    Integer dacVoteId =
1✔
130
        voteDAO.insertVote(user.getUserId(), election.getElectionId(), VoteType.DAC.getValue());
1✔
131
    votes.add(voteDAO.findVoteById(dacVoteId));
1✔
132
    if (isDacChairPerson(dac, user)) {
1✔
133
      Integer chairVoteId =
1✔
134
          voteDAO.insertVote(
1✔
135
              user.getUserId(), election.getElectionId(), VoteType.CHAIRPERSON.getValue());
1✔
136
      votes.add(voteDAO.findVoteById(chairVoteId));
1✔
137
      // Requires Chairperson role to create a final and agreement vote in the Data Access case
138
      if (electionType.equals(ElectionType.DATA_ACCESS)) {
1✔
139
        Integer finalVoteId =
1✔
140
            voteDAO.insertVote(
1✔
141
                user.getUserId(), election.getElectionId(), VoteType.FINAL.getValue());
1✔
142
        votes.add(voteDAO.findVoteById(finalVoteId));
1✔
143
        if (!isManualReview) {
1✔
144
          Integer agreementVoteId =
1✔
145
              voteDAO.insertVote(
1✔
146
                  user.getUserId(), election.getElectionId(), VoteType.AGREEMENT.getValue());
1✔
147
          votes.add(voteDAO.findVoteById(agreementVoteId));
1✔
148
        }
149
      }
150
    }
151
    return votes;
1✔
152
  }
153

154
  public List<Vote> findVotesByIds(List<Integer> voteIds) {
155
    if (voteIds.isEmpty()) {
1✔
156
      return Collections.emptyList();
1✔
157
    }
158
    return voteDAO.findVotesByIds(voteIds);
1✔
159
  }
160

161
  /**
162
   * Delete any votes in Open elections for the specified user in the specified Dac.
163
   *
164
   * @param dac The Dac we are restricting elections to
165
   * @param user The Dac member we are deleting votes for
166
   */
167
  public void deleteOpenDacVotesForUser(Dac dac, User user) {
NEW
168
    List<Integer> openElectionIds =
×
NEW
169
        electionDAO.findOpenElectionsByDacId(dac.getDacId()).stream()
×
NEW
170
            .map(Election::getElectionId)
×
NEW
171
            .toList();
×
172
    if (!openElectionIds.isEmpty()) {
×
NEW
173
      List<Integer> openUserVoteIds =
×
NEW
174
          voteDAO.findVotesByElectionIds(openElectionIds).stream()
×
NEW
175
              .filter(v -> v.getUserId().equals(user.getUserId()))
×
NEW
176
              .map(Vote::getVoteId)
×
NEW
177
              .toList();
×
178
      if (!openUserVoteIds.isEmpty()) {
×
179
        voteDAO.removeVotesByIds(openUserVoteIds);
×
180
      }
181
    }
182
  }
×
183

184
  /**
185
   * Update vote values. 'FINAL' votes impact elections so matching elections marked as
186
   * ElectionStatus.CLOSED as well. Approved 'FINAL' votes trigger an approval email to researchers.
187
   *
188
   * @param votes List of Votes to update
189
   * @param voteValue Value to update the votes to
190
   * @param rationale Value to update the rationales to. Only update if non-null.
191
   * @param user The user making the update
192
   * @return The updated Vote
193
   * @throws IllegalArgumentException when there are non-open, non-rp elections on any of the votes
194
   */
195
  public List<Vote> updateVotesWithValue(
196
      List<Vote> votes, boolean voteValue, String rationale, User user)
197
      throws IllegalArgumentException {
198
    validateVotesCanUpdate(votes);
1✔
199
    try {
200
      List<Vote> updatedVotes = voteServiceDAO.updateVotesWithValue(votes, voteValue, rationale);
1✔
201
      if (voteValue) {
1✔
202
        try {
203
          sendDatasetApprovalNotifications(updatedVotes, user);
1✔
204
        } catch (Exception e) {
×
205
          // We can recover from email errors, log it and don't fail the overall process.
NEW
206
          String voteIds =
×
NEW
207
              votes.stream()
×
NEW
208
                  .map(Vote::getVoteId)
×
NEW
209
                  .map(Object::toString)
×
NEW
210
                  .collect(Collectors.joining(","));
×
UNCOV
211
          String message =
×
212
              "Error notifying researchers and custodians for votes: ["
213
                  + voteIds
214
                  + "]: "
215
                  + e.getMessage();
×
216
          logException(message, e);
×
217
        }
1✔
218
      }
219
      return updatedVotes;
1✔
NEW
220
    } catch (Exception e) {
×
221
      throw new IllegalArgumentException("Unable to update election votes.");
×
222
    }
223
  }
224

225
  /**
226
   * Review all positive, FINAL votes and send a notification to the researcher and data custodians
227
   * describing the approved access to datasets on their Data Access Request.
228
   *
229
   * @param votes List of Vote objects. In practice, this will be a batch of votes for a group of
230
   *     elections for datasets that all have the same data use restriction in a single
231
   *     DarCollection. This method is flexible enough to send email for any number of unrelated
232
   *     elections in various DarCollections.
233
   * @param user The user sending approval notifications
234
   */
235
  public void sendDatasetApprovalNotifications(List<Vote> votes, User user) {
236
    boolean radarApproved =
1✔
237
        votes.stream().anyMatch(v -> VoteType.RADAR_APPROVE.getValue().equals(v.getType()));
1✔
238
    List<Integer> finalElectionIds =
1✔
239
        votes.stream()
1✔
240
            .filter(
1✔
241
                Vote::getVote) // Safety check to ensure we're only emailing for approved election
242
            .filter(
1✔
243
                v ->
244
                    VoteType.FINAL.getValue().equalsIgnoreCase(v.getType())
1✔
245
                        || VoteType.RADAR_APPROVE.getValue().equalsIgnoreCase(v.getType()))
1✔
246
            .map(Vote::getElectionId)
1✔
247
            .distinct()
1✔
248
            .toList();
1✔
249

250
    List<Election> finalElections = electionDAO.findElectionsByIds(finalElectionIds);
1✔
251

252
    List<String> finalElectionReferenceIds =
1✔
253
        finalElections.stream().map(Election::getReferenceId).distinct().toList();
1✔
254

255
    List<DataAccessRequest> dars =
1✔
256
        dataAccessRequestDAO.findByReferenceIds(finalElectionReferenceIds);
1✔
257

258
    List<Integer> datasetIds = finalElections.stream().map(Election::getDatasetId).toList();
1✔
259
    List<Dataset> datasets =
260
        datasetIds.isEmpty() ? List.of() : datasetDAO.findDatasetsByIdList(datasetIds);
1✔
261

262
    try {
263
      elasticSearchService.indexDatasets(datasetIds, user);
1✔
264
    } catch (Exception e) {
×
265
      logException("Error indexing datasets for approved DARs: " + e.getMessage(), e);
×
266
    }
1✔
267

268
    // For each dar, email the researcher summarizing the approved datasets in that dar
269
    dars.forEach(
1✔
270
        dar -> {
271
          // Get the datasets in this collection that have been approved
272
          List<Dataset> approvedDatasetsInDar =
1✔
273
              datasets.stream()
1✔
274
                  .filter(d -> dar.getDatasetIds().contains(d.getDatasetId()))
1✔
275
                  .toList();
1✔
276

277
          if (!approvedDatasetsInDar.isEmpty()) {
1✔
278
            String darCode = dar.getDarCode();
1✔
279
            User researcher = userDAO.findUserById(dar.getUserId());
1✔
280
            Integer researcherId = researcher.getUserId();
1✔
281
            List<DatasetMailDTO> datasetMailDTOs =
1✔
282
                approvedDatasetsInDar.stream()
1✔
283
                    .map(d -> new DatasetMailDTO(d.getName(), d.getDatasetIdentifier()))
1✔
284
                    .toList();
1✔
285

286
            // Get all Data Use translations, distinctly in the case that there are several with the
287
            // same
288
            // data use, and then conjoin them for email display.
289
            String translation =
1✔
290
                approvedDatasetsInDar.stream()
1✔
291
                    .map(
1✔
292
                        dataset ->
293
                            useRestrictionConverter.translateDataUse(
1✔
294
                                dataset.getDataUse(), DataUseTranslationType.DATASET))
1✔
295
                    .distinct()
1✔
296
                    .collect(Collectors.joining(";"));
1✔
297

298
            try {
299
              if (dar.getProgressReport()) {
1✔
300
                emailService.sendResearcherProgressReportApproved(
1✔
301
                    darCode, researcherId, datasetMailDTOs, translation);
302
              } else {
303
                emailService.sendResearcherDarApproved(
1✔
304
                    darCode, researcherId, datasetMailDTOs, translation, radarApproved);
305
              }
NEW
306
            } catch (Exception e) {
×
NEW
307
              logException("Error sending researcher dar approved email: " + e.getMessage(), e);
×
308
            }
1✔
309
            try {
NEW
310
              notifyCustodiansOfApprovedDatasets(
×
311
                  approvedDatasetsInDar, researcher, darCode, radarApproved);
312
            } catch (Exception e) {
1✔
313
              logException(
1✔
314
                  "Error notifying custodians of dar approved email: " + e.getMessage(), e);
1✔
NEW
315
            }
×
316
            try {
317
              notifySigningOfficialsOfApprovedDatasets(
1✔
318
                  approvedDatasetsInDar, researcher, dar, darCode, translation, radarApproved);
NEW
319
            } catch (Exception e) {
×
NEW
320
              logException(
×
NEW
321
                  "Error notifying signing officials of dar approved email: " + e.getMessage(), e);
×
322
            }
1✔
323
            try {
324
              notifyDACOfRadarApprovals(
1✔
325
                  approvedDatasetsInDar, researcher, dar.getReferenceId(), darCode, radarApproved);
1✔
NEW
326
            } catch (Exception e) {
×
NEW
327
              logException("Error notifying DAC of dar approved email: " + e.getMessage(), e);
×
328
            }
1✔
329
          }
330
        });
1✔
331
  }
1✔
332

333
  @VisibleForTesting
334
  protected void notifyDACOfRadarApprovals(
335
      List<Dataset> approvedDatasets,
336
      User researcher,
337
      String referenceId,
338
      String darCode,
339
      boolean radarApproved) {
340
    if (!radarApproved) {
1✔
341
      return;
1✔
342
    }
343
    Map<Integer, Set<DatasetMailDTO>> dacIdToDatasetMap = new HashMap<>();
1✔
344
    approvedDatasets.forEach(
1✔
345
        approvedDataset ->
346
            dacIdToDatasetMap
347
                .computeIfAbsent(approvedDataset.getDacId(), d -> new HashSet<>())
1✔
348
                .add(
1✔
349
                    new DatasetMailDTO(
350
                        approvedDataset.getName(), approvedDataset.getDatasetIdentifier())));
1✔
351
    dacIdToDatasetMap.forEach(
1✔
352
        (dacId, datasets) -> {
353
          List<User> members = dacDAO.findMembersByDacId(dacId);
1✔
354
          members.forEach(
1✔
355
              member -> {
356
                try {
357
                  emailService.sendNewDARRADARApprovalToDAC(
1✔
358
                      member, darCode, referenceId, datasets.stream().toList(), researcher);
1✔
NEW
359
                } catch (TemplateException | IOException e) {
×
NEW
360
                  logWarn("Error sending DAR approval to DAC: " + e.getMessage(), e);
×
361
                }
1✔
362
              });
1✔
363
        });
1✔
364
  }
1✔
365

366
  @VisibleForTesting
367
  protected void notifySigningOfficialsOfApprovedDatasets(
368
      List<Dataset> datasets,
369
      User researcher,
370
      DataAccessRequest dar,
371
      String darCode,
372
      String translation,
373
      boolean radarApproved)
374
      throws TemplateException, IOException {
375
    if (researcher == null) {
1✔
376
      logWarn(
1✔
377
          "Unable to send new DAR/PR message to Signing Officials: Researcher does not exist: %s"
378
              .formatted(dar.getUserId()));
1✔
379
      return;
1✔
380
    }
381
    if (researcher.getInstitutionId() == null) {
1✔
382
      logWarn(
1✔
383
          "Unable to send new DAR/PR message to Signing Officials: Researcher does not have an institution id: %s"
384
              .formatted(dar.getUserId()));
1✔
385
      return;
1✔
386
    }
387
    List<User> signingOfficials = userDAO.getSOsByInstitution(researcher.getInstitutionId());
1✔
388
    for (User so : signingOfficials) {
1✔
389
      if (dar.getProgressReport()) {
1✔
390
        emailService.sendNewSoProgressReportApprovedEmail(
1✔
391
            so, darCode, researcher, dar.getReferenceId(), datasets, translation);
1✔
392
      } else {
393
        emailService.sendNewSoDARApprovedEmail(
1✔
394
            so, darCode, researcher, dar.getReferenceId(), datasets, translation, radarApproved);
1✔
395
      }
396
    }
1✔
397
  }
1✔
398

399
  /**
400
   * Notify all data submitters, custodians, depositors, and owners of a dataset approval.
401
   *
402
   * @param datasets Requested datasets
403
   * @param researcher The approved researcher
404
   * @param darCode The DAR Collection Code
405
   * @throws IllegalArgumentException when there are no custodians or depositors to notify
406
   */
407
  @VisibleForTesting
408
  protected void notifyCustodiansOfApprovedDatasets(
409
      List<Dataset> datasets, User researcher, String darCode, boolean radarApproved)
410
      throws IllegalArgumentException {
411
    Map<User, HashSet<Dataset>> custodianMap = new HashMap<>();
1✔
412

413
    // Find all the data custodians and submitters to notify for each dataset
414
    datasets.forEach(
1✔
415
        d -> {
416
          if (Objects.nonNull(d.getStudy())) {
1✔
417
            Study study = d.getStudy();
1✔
418

419
            // Data Submitter (study)
420
            if (Objects.nonNull(study.getCreateUserId())) {
1✔
421
              User submitter = userDAO.findUserById(study.getCreateUserId());
1✔
422
              if (Objects.nonNull(submitter)) {
1✔
423
                custodianMap.putIfAbsent(submitter, new HashSet<>());
1✔
424
                custodianMap.get(submitter).add(d);
1✔
425
              }
426
            }
427

428
            // Data Custodian (study)
429
            if (Objects.nonNull(study.getProperties())) {
1✔
430
              Type listOfStringType = new TypeToken<ArrayList<String>>() {}.getType();
1✔
431
              Gson gson = GsonUtil.gsonBuilderWithAdapters().create();
1✔
432
              Set<StudyProperty> props = study.getProperties();
1✔
433
              List<String> custodianEmails = new ArrayList<>();
1✔
434
              props.stream()
1✔
435
                  .filter(
1✔
436
                      p -> p.getKey().equals(DatasetRegistrationSchemaV1Builder.dataCustodianEmail))
1✔
437
                  .forEach(
1✔
438
                      p -> {
439
                        String propValue = p.getValue().toString();
1✔
440
                        try {
441
                          custodianEmails.addAll(gson.fromJson(propValue, listOfStringType));
1✔
NEW
442
                        } catch (Exception e) {
×
NEW
443
                          logException(
×
NEW
444
                              "Error finding data custodians for study: " + study.getStudyId(), e);
×
445
                        }
1✔
446
                      });
1✔
447
              if (!custodianEmails.isEmpty()) {
1✔
448
                List<User> custodianUsers = userDAO.findUsersByEmailList(custodianEmails);
1✔
449
                custodianUsers.forEach(
1✔
450
                    s -> {
451
                      custodianMap.putIfAbsent(s, new HashSet<>());
1✔
452
                      custodianMap.get(s).add(d);
1✔
453
                    });
1✔
454
              }
455
            }
456
          }
457

458
          // Data Submitter (dataset)
459
          if (Objects.nonNull(d.getCreateUserId())) {
1✔
460
            User submitter = userDAO.findUserById(d.getCreateUserId());
1✔
461
            if (Objects.nonNull(submitter)) {
1✔
462
              custodianMap.putIfAbsent(submitter, new HashSet<>());
1✔
463
              custodianMap.get(submitter).add(d);
1✔
464
            }
465
          }
466
        });
1✔
467

468
    // Filter out invalid emails in custodian map
469
    EmailValidator emailValidator = EmailValidator.getInstance();
1✔
470
    Map<User, HashSet<Dataset>> validCustodians =
1✔
471
        custodianMap.entrySet().stream()
1✔
472
            .filter(
1✔
473
                e -> e.getKey().getEmail() != null && emailValidator.isValid(e.getKey().getEmail()))
1✔
474
            .collect(
1✔
475
                Collectors.toMap(
1✔
NEW
476
                    Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, HashMap::new));
×
477

478
    if (validCustodians.isEmpty()) {
1✔
479
      String identifiers =
1✔
480
          datasets.stream().map(Dataset::getDatasetIdentifier).collect(Collectors.joining(", "));
1✔
481
      throw new IllegalArgumentException(
1✔
482
          "No submitters, custodians, owners, or depositors found for provided dataset identifiers: "
483
              + identifiers);
484
    }
485
    // For each custodian, notify them of their approved datasets
486
    for (Map.Entry<User, HashSet<Dataset>> entry : validCustodians.entrySet()) {
1✔
487
      List<DatasetMailDTO> datasetMailDTOs =
1✔
488
          entry.getValue().stream()
1✔
489
              .map(d -> new DatasetMailDTO(d.getName(), d.getDatasetIdentifier()))
1✔
490
              .toList();
1✔
491
      try {
492
        emailService.sendDataCustodianApprovalMessage(
1✔
493
            entry.getKey(),
1✔
494
            darCode,
495
            datasetMailDTOs,
496
            entry.getKey().getDisplayName(),
1✔
497
            researcher.getEmail(),
1✔
498
            radarApproved);
499
      } catch (Exception e) {
×
500
        logException("Error sending custodian approval email: " + e.getMessage(), e);
×
501
      }
1✔
502
    }
1✔
503
  }
1✔
504

505
  /**
506
   * The Rationale for RP Votes can be updated for any election status. The Rationale for DataAccess
507
   * Votes can only be updated for OPEN elections. Votes for elections of other types are not
508
   * updatable through this method.
509
   *
510
   * @param voteIds List of vote ids for DataAccess and RP elections
511
   * @param rationale The rationale to update
512
   * @return List of updated votes
513
   * @throws IllegalArgumentException when there are non-open, non-rp elections on any of the votes
514
   */
515
  public List<Vote> updateRationaleByVoteIds(List<Integer> voteIds, String rationale)
516
      throws IllegalArgumentException {
517
    List<Vote> votes = voteDAO.findVotesByIds(voteIds);
1✔
518
    validateVotesCanUpdate(votes);
1✔
519
    voteDAO.updateRationaleByVoteIds(voteIds, rationale);
1✔
520
    return findVotesByIds(voteIds);
1✔
521
  }
522

523
  private void validateVotesCanUpdate(List<Vote> votes) throws ConsentConflictException {
524
    List<Election> elections =
1✔
525
        electionDAO.findElectionsByIds(votes.stream().map(Vote::getElectionId).toList());
1✔
526

527
    // If there are any DataAccess elections in a non-open state, throw an error
528
    List<Election> nonOpenAccessElections =
1✔
529
        elections.stream()
1✔
530
            .filter(
1✔
531
                election -> election.getElectionType().equals(ElectionType.DATA_ACCESS.getValue()))
1✔
532
            .filter(election -> !election.getStatus().equals(ElectionStatus.OPEN.getValue()))
1✔
533
            .toList();
1✔
534
    if (!nonOpenAccessElections.isEmpty()) {
1✔
535
      throw new ConsentConflictException(
1✔
536
          "One or more of these votes are associated with elections not open for voting.");
537
    }
538

539
    // If there are non-DataAccess or non-RP elections, throw an error
540
    List<Election> disallowedElections =
1✔
541
        elections.stream()
1✔
542
            .filter(
1✔
543
                election -> !election.getElectionType().equals(ElectionType.DATA_ACCESS.getValue()))
1✔
544
            .filter(election -> !election.getElectionType().equals(ElectionType.RP.getValue()))
1✔
545
            .toList();
1✔
546
    if (!disallowedElections.isEmpty()) {
1✔
547
      throw new ConsentConflictException(
1✔
548
          "There are unsupported election types for the votes provided");
549
    }
550
  }
1✔
551

552
  private boolean isDacChairPerson(Dac dac, User user) {
553
    if (dac != null) {
1✔
NEW
554
      return user.getRoles().stream()
×
NEW
555
          .anyMatch(
×
556
              userRole ->
NEW
557
                  Objects.nonNull(userRole.getRoleId())
×
NEW
558
                      && Objects.nonNull(userRole.getDacId())
×
NEW
559
                      && userRole.getRoleId().equals(UserRoles.CHAIRPERSON.getRoleId())
×
NEW
560
                      && userRole.getDacId().equals(dac.getDacId()));
×
561
    }
562
    return user.getRoles().stream()
1✔
563
        .anyMatch(
1✔
564
            userRole ->
565
                Objects.nonNull(userRole.getRoleId())
1✔
566
                    && userRole.getRoleId().equals(UserRoles.CHAIRPERSON.getRoleId()));
1✔
567
  }
568

569
  public void logDARApprovalOrRejection(
570
      User user, List<Vote> updatedVotes, ContainerRequest request) {
571
    List<Integer> approvedElectionIds =
1✔
572
        updatedVotes.stream()
1✔
573
            .filter(v -> v.getType().equals(VoteType.FINAL.getValue()))
1✔
574
            .filter(Vote::getVote)
1✔
575
            .map(Vote::getElectionId)
1✔
576
            .toList();
1✔
577
    List<Integer> approvedDatasetIds =
1✔
578
        electionDAO.findElectionsByIds(approvedElectionIds).stream()
1✔
579
            .map(Election::getDatasetId)
1✔
580
            .toList();
1✔
581
    List<Dataset> approvedDatasets = datasetDAO.findDatasetsByIdList(approvedDatasetIds);
1✔
582
    ComplianceLogger.logDARApproval(
1✔
583
        user, approvedDatasets, request, HttpStatusCodes.STATUS_CODE_OK);
584

585
    List<Integer> rejectedElectionIds =
1✔
586
        updatedVotes.stream()
1✔
587
            .filter(v -> v.getType().equals(VoteType.FINAL.getValue()))
1✔
588
            .filter(not(Vote::getVote))
1✔
589
            .map(Vote::getElectionId)
1✔
590
            .toList();
1✔
591
    List<Integer> rejectedDatasetIds =
1✔
592
        electionDAO.findElectionsByIds(rejectedElectionIds).stream()
1✔
593
            .map(Election::getDatasetId)
1✔
594
            .toList();
1✔
595
    List<Dataset> rejectedDatasets = datasetDAO.findDatasetsByIdList(rejectedDatasetIds);
1✔
596
    ComplianceLogger.logDARRejection(
1✔
597
        user, rejectedDatasets, request, HttpStatusCodes.STATUS_CODE_OK);
598
  }
1✔
599
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc