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

DataBiosphere / consent / #5929

19 May 2025 12:36PM UTC coverage: 78.551% (-0.2%) from 78.727%
#5929

push

web-flow
DT-1564, Progress Report DAC flow (#2523)

175 of 228 new or added lines in 17 files covered. (76.75%)

5 existing lines in 3 files now uncovered.

10027 of 12765 relevant lines covered (78.55%)

0.79 hits per line

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

91.91
/src/main/java/org/broadinstitute/consent/http/service/DarCollectionService.java
1
package org.broadinstitute.consent.http.service;
2

3
import static java.util.stream.Collectors.toList;
4

5
import com.google.common.annotations.VisibleForTesting;
6
import com.google.common.collect.Streams;
7
import com.google.gson.Gson;
8
import com.google.inject.Inject;
9
import freemarker.template.TemplateException;
10
import jakarta.ws.rs.BadRequestException;
11
import jakarta.ws.rs.NotAcceptableException;
12
import jakarta.ws.rs.NotAuthorizedException;
13
import jakarta.ws.rs.NotFoundException;
14
import java.io.IOException;
15
import java.text.SimpleDateFormat;
16
import java.util.ArrayList;
17
import java.util.Collection;
18
import java.util.Collections;
19
import java.util.Date;
20
import java.util.HashMap;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Objects;
24
import java.util.Set;
25
import java.util.function.Function;
26
import java.util.function.Predicate;
27
import java.util.stream.Collectors;
28
import java.util.stream.Stream;
29
import org.broadinstitute.consent.http.db.DacDAO;
30
import org.broadinstitute.consent.http.db.DarCollectionDAO;
31
import org.broadinstitute.consent.http.db.DarCollectionSummaryDAO;
32
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
33
import org.broadinstitute.consent.http.db.DatasetDAO;
34
import org.broadinstitute.consent.http.db.ElectionDAO;
35
import org.broadinstitute.consent.http.db.MatchDAO;
36
import org.broadinstitute.consent.http.db.UserDAO;
37
import org.broadinstitute.consent.http.db.VoteDAO;
38
import org.broadinstitute.consent.http.enumeration.DarCollectionActions;
39
import org.broadinstitute.consent.http.enumeration.DarCollectionStatus;
40
import org.broadinstitute.consent.http.enumeration.DarStatus;
41
import org.broadinstitute.consent.http.enumeration.ElectionStatus;
42
import org.broadinstitute.consent.http.enumeration.UserRoles;
43
import org.broadinstitute.consent.http.enumeration.VoteType;
44
import org.broadinstitute.consent.http.mail.message.NewCaseMessage;
45
import org.broadinstitute.consent.http.mail.message.NewDARRequestMessage;
46
import org.broadinstitute.consent.http.mail.message.NewProgressReportCaseMessage;
47
import org.broadinstitute.consent.http.mail.message.NewProgressReportRequestMessage;
48
import org.broadinstitute.consent.http.models.Dac;
49
import org.broadinstitute.consent.http.models.DarCollection;
50
import org.broadinstitute.consent.http.models.DarCollectionSummary;
51
import org.broadinstitute.consent.http.models.DataAccessRequest;
52
import org.broadinstitute.consent.http.models.DataAccessRequestData;
53
import org.broadinstitute.consent.http.models.Dataset;
54
import org.broadinstitute.consent.http.models.Election;
55
import org.broadinstitute.consent.http.models.User;
56
import org.broadinstitute.consent.http.models.UserRole;
57
import org.broadinstitute.consent.http.models.Vote;
58
import org.broadinstitute.consent.http.service.dao.DarCollectionServiceDAO;
59
import org.broadinstitute.consent.http.util.ConsentLogger;
60

61
public class DarCollectionService implements ConsentLogger {
62

63
  private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
1✔
64
  private final DarCollectionDAO darCollectionDAO;
65
  private final DarCollectionServiceDAO collectionServiceDAO;
66
  private final DacDAO dacDAO;
67
  private final DarCollectionSummaryDAO darCollectionSummaryDAO;
68
  private final DataAccessRequestDAO dataAccessRequestDAO;
69
  private final DatasetDAO datasetDAO;
70
  private final ElectionDAO electionDAO;
71
  private final EmailService emailService;
72
  private final MatchDAO matchDAO;
73
  private final UserDAO userDAO;
74
  private final VoteDAO voteDAO;
75

76
  @Inject
77
  public DarCollectionService(DarCollectionDAO darCollectionDAO,
78
      DarCollectionServiceDAO collectionServiceDAO, DatasetDAO datasetDAO, ElectionDAO electionDAO,
79
      DataAccessRequestDAO dataAccessRequestDAO, EmailService emailService, VoteDAO voteDAO,
80
      MatchDAO matchDAO, DarCollectionSummaryDAO darCollectionSummaryDAO, UserDAO userDAO, DacDAO dacDAO) {
1✔
81
    this.darCollectionDAO = darCollectionDAO;
1✔
82
    this.collectionServiceDAO = collectionServiceDAO;
1✔
83
    this.datasetDAO = datasetDAO;
1✔
84
    this.electionDAO = electionDAO;
1✔
85
    this.dataAccessRequestDAO = dataAccessRequestDAO;
1✔
86
    this.emailService = emailService;
1✔
87
    this.voteDAO = voteDAO;
1✔
88
    this.matchDAO = matchDAO;
1✔
89
    this.darCollectionSummaryDAO = darCollectionSummaryDAO;
1✔
90
    this.userDAO = userDAO;
1✔
91
    this.dacDAO = dacDAO;
1✔
92
  }
1✔
93

94
  private void updateStatusCount(Map<String, Integer> statusCount, String status) {
95
    // If the status is null, track it as Undefined to ensure election is accounted for.
96
    statusCount.merge(Objects.requireNonNullElse(status, "Undefined"), 1, Integer::sum);
1✔
97
  }
1✔
98

99
  private void determineCollectionStatus(DarCollectionSummary summary,
100
      Map<String, Integer> statusCount, Integer datasetCount, Integer electionCount) {
101
    //If there are no elections, status is unreviewed
102
    //if there are some elections open, status is in process
103
    //if all elections are closed or canceled and electionCount == datasetCount, status is complete
104
    if (electionCount.equals(0)) {
1✔
105
      summary.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
106
    } else if (electionCount.equals(datasetCount)) {
1✔
107
      Integer openCount = statusCount.get(ElectionStatus.OPEN.getValue());
1✔
108
      if (Objects.isNull(openCount)) {
1✔
109
        summary.setStatus(DarCollectionStatus.COMPLETE.getValue());
1✔
110
      } else {
111
        summary.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
112
      }
113
    } else {
1✔
114
      summary.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
115
    }
116
  }
1✔
117

118
  private void processDarCollectionSummariesForAdmin(List<DarCollectionSummary> summaries) {
119
    //if at least one election is open, show cancel
120
    //if at least one non-open/absent election, show open
121
    summaries.forEach(s -> {
1✔
122
      Map<String, Integer> statusCount = new HashMap<>();
1✔
123
      Map<Integer, Election> elections = s.getElections();
1✔
124
      if (elections.isEmpty()) {
1✔
125
        s.addAction(DarCollectionActions.OPEN);
1✔
126
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
127
      } else {
128
        elections.values().forEach(e -> {
1✔
129
          String status = e.getStatus();
1✔
130
          updateStatusCount(statusCount, status);
1✔
131
          if (status.equals(ElectionStatus.OPEN.getValue())) {
1✔
132
            s.addAction(DarCollectionActions.CANCEL);
1✔
133
          } else {
134
            s.addAction(DarCollectionActions.OPEN);
1✔
135
          }
136
        });
1✔
137
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
138
      }
139
    });
1✔
140
  }
1✔
141

142
  private DarCollectionSummary processDraftAsSummary(DataAccessRequest d) {
143
    try {
144
      DarCollectionSummary summary = new DarCollectionSummary();
1✔
145
      String darCode = "DRAFT_DAR_" + sdf.format(d.getCreateDate());
1✔
146
      summary.setDarCode(darCode);
1✔
147
      summary.setStatus(DarCollectionStatus.DRAFT.getValue());
1✔
148
      summary.setName(d.getData().getProjectTitle());
1✔
149
      summary.addAction(DarCollectionActions.RESUME);
1✔
150
      summary.addAction(DarCollectionActions.DELETE);
1✔
151
      summary.addReferenceId(d.referenceId);
1✔
152
      return summary;
1✔
153
    } catch (Exception e) {
×
154
      logWarn("Error processing draft with id: %s".formatted(d.getId()), e);
×
155
    }
156
    return null;
×
157
  }
158

159
  private void processDarCollectionSummariesForResearcher(List<DarCollectionSummary> summaries) {
160
    //if an election exists, cancel does not appear
161
    //if there are no elections, review and cancel are present
162
    //if the collection is canceled, revise and review is present
163
    summaries.forEach(s -> {
1✔
164
      Map<String, Integer> statusCount = new HashMap<>();
1✔
165
      Map<Integer, Election> elections = s.getElections();
1✔
166
      int electionCount = elections.size();
1✔
167
      elections.values().forEach(election -> updateStatusCount(statusCount, election.getStatus()));
1✔
168
      s.addAction(DarCollectionActions.REVIEW);
1✔
169
      //if any DARs in the collection have approved datasets, include create progress report action
170
      Set<Integer> datasetIds = dataAccessRequestDAO.findDatasetApprovalsByDars(List.copyOf(s.getReferenceIds()));
1✔
171
      if (!datasetIds.isEmpty()) {
1✔
172
        s.addAction(DarCollectionActions.CREATE_PROGRESS_REPORT);
1✔
173
      }
174
      //check dar statuses, if they're all canceled show revise (but only if there are no elections)
175
      if (electionCount == 0) {
1✔
176
        Collection<String> darStatuses = s.getDarStatuses().values();
1✔
177
        boolean isCanceled = !darStatuses.isEmpty() && darStatuses.stream()
1✔
178
            .allMatch(st -> st.equalsIgnoreCase(DarStatus.CANCELED.getValue()));
1✔
179
        if (isCanceled) {
1✔
180
          s.addAction(DarCollectionActions.REVISE);
1✔
181
          s.setStatus(DarCollectionStatus.CANCELED.getValue());
1✔
182
        } else {
183
          if (!s.getProgressReport()) {
1✔
184
            s.addAction(DarCollectionActions.CANCEL);
1✔
185
          }
186
          s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
187
        }
188
      } else {
1✔
189
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
190
      }
191
    });
1✔
192
  }
1✔
193

194
  private void processDarCollectionSummariesForMember(List<DarCollectionSummary> summaries,
195
      Integer userId) {
196
    summaries.forEach(s -> {
1✔
197
      Collection<Election> elections = s.getElections().values();
1✔
198
      Integer electionCount = elections.size();
1✔
199
      //if there are no elections present, unreviewed
200
      //if there are elections present. in process
201
      if (electionCount == 0) {
1✔
202
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
203
      } else {
204
        boolean isVotable = elections
1✔
205
            .stream()
1✔
206
            .anyMatch(
1✔
207
                election -> election.getStatus().equalsIgnoreCase(ElectionStatus.OPEN.getValue()));
1✔
208

209
        if (isVotable) {
1✔
210
          s.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
211
          List<Vote> votes = s.getVotes().stream()
1✔
212
              .filter(
1✔
213
                  v -> v.getUserId().equals(userId) && v.getType().equals(VoteType.DAC.getValue()))
1✔
214
              .toList();
1✔
215
          if (!votes.isEmpty()) {
1✔
216
            boolean hasVoted = votes.stream().map(Vote::getVote).allMatch(Objects::nonNull);
1✔
217
            DarCollectionActions targetAction = hasVoted ? DarCollectionActions.UPDATE
1✔
218
                : DarCollectionActions.VOTE;
1✔
219
            s.addAction(targetAction);
1✔
220
          }
221
        } else {
1✔
222
          //non-votable states
223
          //all canceled (complete)
224
          //some datasets do not have elections (in process)
225
          //all voted on (complete)
226
          //no elections
227
          if (electionCount < s.getDatasetCount()) {
1✔
228
            s.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
×
229
          } else {
230
            s.setStatus(DarCollectionStatus.COMPLETE.getValue());
1✔
231
          }
232
        }
233
      }
234
    });
1✔
235
  }
1✔
236

237

238
  private void processDarCollectionSummariesForChair(List<DarCollectionSummary> summaries) {
239
    summaries.forEach(s -> {
1✔
240
      //if there are no elections, only show open
241
      //if there is any closed or canceled elections, or if some datasets dont have an election, show open
242
      //if there are any open elections, show cancel and vote
243
      Map<String, Integer> statusCount = new HashMap<>();
1✔
244
      Map<Integer, Election> elections = s.getElections();
1✔
245
      if (elections.size() == 0) {
1✔
246
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
247
        s.addAction(DarCollectionActions.OPEN);
1✔
248
      } else {
249
        if (elections.size() < s.getDatasetCount()) {
1✔
250
          s.addAction(DarCollectionActions.OPEN);
1✔
251
        }
252
        elections.values().forEach(election -> {
1✔
253
          String statusString = election.getStatus();
1✔
254
          updateStatusCount(statusCount, statusString);
1✔
255
          ElectionStatus status = ElectionStatus.getStatusFromString(statusString);
1✔
256
          switch (status) {
1✔
257
            case CLOSED, CANCELED:
258
              s.addAction(DarCollectionActions.OPEN);
1✔
259
              break;
1✔
260
            case OPEN:
261
              s.addAction(DarCollectionActions.VOTE);
1✔
262
              break;
1✔
263
            default:
264
              break;
265
          }
266
        });
1✔
267
        Integer closedCount = statusCount.get(ElectionStatus.CLOSED.getValue());
1✔
268
        Integer openCount = statusCount.get(ElectionStatus.OPEN.getValue());
1✔
269
        //add cancel if there are no closed elections and at least one open election
270
        if (Objects.isNull(closedCount) && Objects.nonNull(openCount)) {
1✔
271
          s.addAction(DarCollectionActions.CANCEL);
1✔
272
        }
273

274
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
275
      }
276
    });
1✔
277
  }
1✔
278

279
  private void processDarCollectionSummariesForSO(List<DarCollectionSummary> summaries) {
280
    summaries.forEach(s -> {
1✔
281
      Map<String, Integer> statusCount = new HashMap<>();
1✔
282
      s.getElections().values()
1✔
283
          .forEach(election -> updateStatusCount(statusCount, election.getStatus()));
1✔
284
      determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
285
    });
1✔
286
  }
1✔
287

288
  /**
289
   * Find all DarCollectionSummaries for a given role. Admins can see all summaries Chairs and
290
   * Members can see summaries for datasets they have access to Signing Officials can see summaries
291
   * for researchers in their institution Researchers can see only their own summaries
292
   *
293
   * @param user     The user making the request
294
   * @param role The role the user is making the request as
295
   * @return List of DarCollectionSummary objects
296
   */
297
  public List<DarCollectionSummary> getSummariesForRole(User user, UserRoles role) {
298
    final List<DarCollectionSummary> summaries;
299
    Integer userId = user.getUserId();
1✔
300
    List<Integer> datasetIds;
301
    switch (role) {
1✔
302
      case ADMIN:
303
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForAdmin();
1✔
304
        processDarCollectionSummariesForAdmin(summaries);
1✔
305
        break;
1✔
306
      case SIGNINGOFFICIAL:
307
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForSO(user.getInstitutionId());
1✔
308
        processDarCollectionSummariesForSO(summaries);
1✔
309
        break;
1✔
310
      case CHAIRPERSON:
311
        datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.CHAIRPERSON.getRoleId());
1✔
312
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForDAC(userId, datasetIds);
1✔
313
        processDarCollectionSummariesForChair(summaries);
1✔
314
        break;
1✔
315
      case MEMBER:
316
        datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.MEMBER.getRoleId());
1✔
317
        summaries = darCollectionSummaryDAO.getDarCollectionSummariesForDAC(userId, datasetIds);
1✔
318
        processDarCollectionSummariesForMember(summaries, userId);
1✔
319
        break;
1✔
320
      case RESEARCHER:
321
        var darSummaries = darCollectionSummaryDAO.getDarCollectionSummariesForResearcher(userId);
1✔
322
        processDarCollectionSummariesForResearcher(darSummaries);
1✔
323
        List<DataAccessRequest> drafts = dataAccessRequestDAO.findAllDraftsByUserId(userId);
1✔
324
        summaries =
1✔
325
            Stream.concat(
1✔
326
                    darSummaries.stream(),
1✔
327
                    drafts.stream().map(this::processDraftAsSummary).filter(Objects::nonNull))
1✔
328
                .toList();
1✔
329
        break;
1✔
330
      default:
331
        summaries = List.of();
×
332
        break;
333
    }
334
    return summaries;
1✔
335
  }
336

337
  private List<Integer> getDatasetIdsForUserAndRoleId(User user, Integer roleId) {
338
    List<Integer> roleDacIds = user.getRoles().stream()
1✔
339
        .filter(ur -> Objects.nonNull(ur.getRoleId()))
1✔
340
        .filter(ur -> ur.getRoleId().equals(roleId))
1✔
341
        .map(UserRole::getDacId)
1✔
342
        .filter(Objects::nonNull)
1✔
343
        .toList();
1✔
344
    return Stream.of(roleDacIds)
1✔
345
        .filter(Predicate.not(List::isEmpty))
1✔
346
        .map(datasetDAO::findDatasetListByDacIds)
1✔
347
        .flatMap(List::stream)
1✔
348
        .map(Dataset::getDatasetId)
1✔
349
        .toList();
1✔
350
  }
351

352
  /**
353
   * Finds the DarCollectionSummary for a given darCollectionId, processed by the given role.
354
   *
355
   * @param user         The user making the request
356
   * @param role         The role the user is making the request as
357
   * @param collectionId The darCollectionId of the requested DarCollectionSummary
358
   * @return A DarCollectionSummary object
359
   */
360
  public DarCollectionSummary getSummaryForRoleByCollectionId(User user, UserRoles role,
361
      Integer collectionId) {
362
    DarCollectionSummary summary = null;
1✔
363
    Integer userId = user.getUserId();
1✔
364
    List<Integer> datasetIds;
365
    try {
366
      switch (role) {
1✔
367
        case ADMIN:
368
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
369
          processDarCollectionSummariesForAdmin(List.of(summary));
1✔
370
          break;
1✔
371
        case SIGNINGOFFICIAL:
372
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
373
          processDarCollectionSummariesForSO(List.of(summary));
1✔
374
          break;
1✔
375
        case CHAIRPERSON:
376
          datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.CHAIRPERSON.getRoleId());
1✔
377
          summary = darCollectionSummaryDAO.getDarCollectionSummaryForDACByCollectionId(userId,
1✔
378
              datasetIds, collectionId);
379
          processDarCollectionSummariesForChair(List.of(summary));
1✔
380
          break;
1✔
381
        case MEMBER:
382
          datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.MEMBER.getRoleId());
1✔
383
          summary = darCollectionSummaryDAO.getDarCollectionSummaryForDACByCollectionId(userId,
1✔
384
              datasetIds, collectionId);
385
          processDarCollectionSummariesForMember(List.of(summary), userId);
1✔
386
          break;
1✔
387
        case RESEARCHER:
388
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
389
          processDarCollectionSummariesForResearcher(List.of(summary));
1✔
390
          break;
1✔
391
        default:
392
          break;
393
      }
394
      return summary;
1✔
395
    } catch (NullPointerException e) {
1✔
396
      throw new NotFoundException(
1✔
397
          "Collection summary with the collection id of " + collectionId + " was not found");
398
    }
399
  }
400

401
  public DarCollectionSummary updateCollectionToDraftStatus(DarCollection sourceCollection) {
402
    sourceCollection.getDars().values().forEach((d) -> {
×
403
      Date now = new Date();
×
404
      DataAccessRequestData newData = new Gson().fromJson(d.getData().toString(),
×
405
          DataAccessRequestData.class);
406
      newData.setDarCode(null);
×
407
      newData.setStatus(null);
×
408
      newData.setReferenceId(d.getReferenceId());
×
409
      newData.setSortDate(now.getTime());
×
410
      dataAccessRequestDAO.updateDataByReferenceId(
×
411
          d.getReferenceId(),
×
412
          d.getUserId(),
×
413
          now,
414
          null,
415
          now,
416
          newData,
417
          null
418
      );
419
    });
×
420

421
    // get updated collection
422
    sourceCollection = this.darCollectionDAO.findDARCollectionByCollectionId(
×
423
        sourceCollection.getDarCollectionId());
×
424

425
    return this.processDraftAsSummary(new ArrayList<>(sourceCollection.getDars().values()).get(0));
×
426
  }
427

428
  /**
429
   * Find all dataset ids by the DAC User. Will return ids for Chairpersons or Members
430
   *
431
   * @param user The DAC User
432
   * @return List of Dataset IDs
433
   */
434
  public List<Integer> findDatasetIdsByDACUser(User user) {
435
    return datasetDAO.findDatasetIdsByDACUserId(user.getUserId());
×
436
  }
437

438
  public void deleteByCollectionId(User user, Integer collectionId)
439
      throws NotAcceptableException, NotAuthorizedException, NotFoundException {
440
    DarCollection coll = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
441
    if (coll == null) {
1✔
442
      throw new NotFoundException("DAR Collection does not exist at that id.");
1✔
443
    }
444

445
    // ensure the user is capable of deleting the collection
446
    if (!user.hasUserRole(UserRoles.ADMIN) && !coll.getCreateUserId().equals(user.getUserId())) {
1✔
447
      throw new NotAuthorizedException("Not authorized to delete DAR Collection.");
1✔
448
    }
449

450
    // get the reference ids of the dars in the collection
451
    List<String> referenceIds =
1✔
452
        coll.getDars().values().stream().map(DataAccessRequest::getReferenceId).distinct()
1✔
453
            .collect(toList());
1✔
454

455
    // ensure there are no elections; if there are, will attempt to delete (must be admin)
456
    ensureNoElections(user, referenceIds);
1✔
457

458
    // no elections left & user has perms => safe to delete collection
459

460
    // delete DARs
461
    matchDAO.deleteRationalesByPurposeIds(referenceIds);
1✔
462
    matchDAO.deleteMatchesByPurposeIds(referenceIds);
1✔
463
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceIds(referenceIds);
1✔
464
    dataAccessRequestDAO.deleteByReferenceIds(referenceIds);
1✔
465

466
    // delete collection
467
    darCollectionDAO.deleteByCollectionId(collectionId);
1✔
468
  }
1✔
469

470
  // checks if there are any elections for any of the DARs in the referenceIds; if so,
471
  // will attempt to delete them (must be admin to delete)
472
  private void ensureNoElections(User user, List<String> referenceIds)
473
      throws NotAcceptableException {
474
    // get elections across all reference ids
475
    List<Election> allElections = electionDAO.findElectionsByReferenceIds(referenceIds);
1✔
476

477
    // if there are already no elections, we're done!
478
    if (allElections.isEmpty()) {
1✔
479
      return;
1✔
480
    }
481

482
    // if there are any elections, we need to delete them.
483
    // only admins can delete elections; make sure user is an admin
484
    if (!user.hasUserRole(UserRoles.ADMIN)) {
1✔
485
      throw new NotAcceptableException("Cannot delete DAR with elections.");
1✔
486
    }
487

488
    // delete all votes
489
    voteDAO.deleteVotesByReferenceIds(referenceIds);
1✔
490

491
    // delete all elections
492
    List<Integer> electionIds = allElections.stream().map(Election::getElectionId)
1✔
493
        .collect(toList());
1✔
494

495
    electionDAO.deleteElectionsByIds(electionIds);
1✔
496

497
  }
1✔
498

499
  public DarCollection getByReferenceId(String referenceId) {
500
    DarCollection collection = darCollectionDAO.findDARCollectionByReferenceId(referenceId);
×
501
    if (Objects.isNull(collection)) {
×
502
      throw new NotFoundException(
×
503
          "Collection with the reference id of " + referenceId + " was not found");
504
    }
505
    return addDatasetsToCollection(collection);
×
506
  }
507

508
  public DarCollection getByCollectionId(Integer collectionId) {
509
    DarCollection collection = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
510
    if (Objects.isNull(collection)) {
1✔
511
      throw new NotFoundException(
×
512
          "Collection with the collection id of " + collectionId + " was not found");
513
    }
514
    return addDatasetsToCollection(collection);
1✔
515
  }
516

517
  /**
518
   * Given a DarCollection, add its relevant datasets.
519
   *
520
   * @param collection      The list of DarCollections to iterate over.
521
   * @return collection with datasets added
522
   */
523
  @VisibleForTesting
524
  protected DarCollection addDatasetsToCollection(DarCollection collection) {
525
    // get datasetIds from each DAR from each collection
526
    List<String> referenceIds = List.copyOf(collection.getDars().keySet());
1✔
527
    List<Integer> datasetIds = referenceIds.isEmpty() ? List.of()
1✔
528
        : dataAccessRequestDAO.findAllDARDatasetRelations(referenceIds);
1✔
529
    if (!datasetIds.isEmpty()) {
1✔
530
      Map<Integer, Dataset> datasetMap = datasetDAO.findDatasetsByIdList(datasetIds)
1✔
531
          .stream()
1✔
532
          .distinct()
1✔
533
          .collect(Collectors.toMap(Dataset::getDatasetId, Function.identity()));
1✔
534

535
        Set<Dataset> collectionDatasets = collection.getDars().values().stream()
1✔
536
            .map(DataAccessRequest::getDatasetIds)
1✔
537
            .flatMap(Collection::stream)
1✔
538
            .map(datasetMap::get)
1✔
539
            .filter(Objects::nonNull) // filtering out nulls which were getting captured by map
1✔
540
            .collect(Collectors.toSet());
1✔
541
        DarCollection copy = collection.deepCopy();
1✔
542
        copy.setDatasets(collectionDatasets);
1✔
543
        return copy;
1✔
544
    }
545
    // There were no datasets to add, so we return the original list
546
    return collection;
1✔
547
  }
548

549
  /**
550
   * Cancel Elections or a dar for a DarCollection, given a user and a role. If the user is a chair,
551
   * or admin, cancel elections. If the user is a researcher, cancel the dar.
552
   *
553
   * @param user       The User initiating the cancel
554
   * @param collection The DarCollection
555
   * @param role       The role of the user, must be one of ADMIN, CHAIRPERSON, or RESEARCHER
556
   * @return The DarCollection that has been canceled
557
   */
558
  public DarCollection cancelDarCollectionByRole(User user, DarCollection collection, UserRoles role) {
559
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
560
    if (dars.isEmpty()) {
1✔
561
      logWarn("DAR Collection ID: [%s] does not have any associated DAR ids".formatted(
1✔
562
          collection.getDarCollectionId()));
1✔
563
      return collection;
1✔
564
    }
565

566
    return switch (role) {
1✔
567
      case ADMIN -> cancelDarCollectionElectionsAsAdmin(collection);
1✔
568
      case CHAIRPERSON ->
569
          cancelDarCollectionElectionsAsChair(collection, user);
1✔
570
      default -> cancelDarCollectionAsResearcher(collection, user);
1✔
571
    };
572
  }
573

574
  /**
575
   * Cancel a DarCollection as a researcher.
576
   * <p>
577
   * If an election exists for a DAR within the collection, that DAR cannot be cancelled by the
578
   * researcher. Since it's now under DAC review, it's up to the DAC Chair (or admin) to ultimately
579
   * decline or cancel the elections for the collection.
580
   *
581
   * @param collection The DarCollection
582
   * @param user the researcher requesting the cancel
583
   * @return The canceled DarCollection
584
   */
585
  private DarCollection cancelDarCollectionAsResearcher(DarCollection collection, User user) {
586
    if (!user.getUserId().equals(collection.getCreateUserId())) {
1✔
587
      throw new NotFoundException();
×
588
    }
589
    DarCollectionSummary summary = darCollectionSummaryDAO
1✔
590
        .getDarCollectionSummaryByCollectionId(collection.getDarCollectionId());
1✔
591
    if (summary.getProgressReport()) {
1✔
592
      throw new BadRequestException("Cannot cancel a progress report");
1✔
593
    }
594

595
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
596
    List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();
1✔
597

598
    List<Election> elections = electionDAO.findLastElectionsByReferenceIds(referenceIds);
1✔
599
    if (!elections.isEmpty()) {
1✔
600
      throw new BadRequestException("Elections present on DARs; cannot cancel collection");
1✔
601
    }
602

603
    // Cancel active dars for the researcher
604
    List<String> activeDarIds = dars.stream()
1✔
605
        .filter(d -> !DataAccessRequest.isCanceled(d))
1✔
606
        .map(DataAccessRequest::getReferenceId)
1✔
607
        .toList();
1✔
608
    if (!activeDarIds.isEmpty()) {
1✔
609
      dataAccessRequestDAO.cancelByReferenceIds(activeDarIds);
1✔
610
    }
611

612
    return getByCollectionId(collection.getDarCollectionId());
1✔
613
  }
614

615
  /**
616
   * Cancel Elections for a DarCollection as an admin.
617
   * <p>
618
   * Admins can cancel all elections in a DarCollection
619
   *
620
   * @param collection The DarCollection
621
   * @return The DarCollection whose elections have been canceled
622
   */
623
  private DarCollection cancelDarCollectionElectionsAsAdmin(DarCollection collection) {
624
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
625
    List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();
1✔
626

627
    // Cancel all DAR elections
628
    cancelElectionsForReferenceIds(referenceIds);
1✔
629

630
    return getByCollectionId(collection.getDarCollectionId());
1✔
631
  }
632

633
  /**
634
   * Cancel Elections for a DarCollection as a chairperson.
635
   * <p>
636
   * Chairs can only cancel Elections that reference a dataset the chair is a DAC member for.
637
   *
638
   * @param collection The DarCollection
639
   * @return The DarCollection whose elections have been canceled
640
   */
641
  private DarCollection cancelDarCollectionElectionsAsChair(DarCollection collection, User user) {
642
    // Find dataset ids the chairperson has access to:
643
    Set<Integer> datasetIds = Set.copyOf(datasetDAO.findDatasetIdsByDACUserId(user.getUserId()));
1✔
644

645
    // Filter the list of DARs we can operate on by the datasets accessible to this chairperson
646
    List<String> referenceIds = collection.getDars().values().stream()
1✔
647
        .filter(d -> datasetIds.containsAll(d.getDatasetIds()))
1✔
648
        .map(DataAccessRequest::getReferenceId)
1✔
649
        .toList();
1✔
650

651
    if (referenceIds.isEmpty()) {
1✔
652
      logWarn(
1✔
653
          "DAR Collection ID: [%s] does not have any associated DARs that this chairperson can access".formatted(
1✔
654
              collection.getDarCollectionId()));
1✔
655
      return collection;
1✔
656
    }
657

658
    // Cancel filtered DAR elections
659
    cancelElectionsForReferenceIds(referenceIds);
1✔
660

661
    return getByCollectionId(collection.getDarCollectionId());
1✔
662
  }
663

664
  /**
665
   * DarCollections with no elections, or with previously canceled elections, are valid for
666
   * initiating a new set of elections. Elections in open, closed, pending, or final states are not
667
   * valid.
668
   *
669
   * @param user       The User initiating new elections for a collection
670
   * @param collection The DarCollection
671
   * @return The updated DarCollection
672
   */
673
  public DarCollection createElectionsForDarCollection(User user, DarCollection collection)
674
      throws Exception {
675
    try {
676
      DataAccessRequest dar = collection.getMostRecentDar();
1✔
677
      List<String> createdElectionReferenceIds = collectionServiceDAO.createElectionsForDarByUser(
1✔
678
          user, dar);
679
      if (createdElectionReferenceIds.isEmpty()) {
1✔
680
        var e = new IllegalStateException(
1✔
681
            "No elections were created for DAR Collection: %s %s".formatted(
1✔
682
                collection.getDarCode(), dar.getReferenceId()));
1✔
683
        logException(e);
1✔
684
        throw e;
1✔
685
      }
686
      try {
687
        List<User> voteUsers = voteDAO.findVoteUsersByElectionReferenceIdList(
1✔
688
            createdElectionReferenceIds);
689
        if (dar.getProgressReport()) {
1✔
690
          emailService.sendProgressReportNewCollectionElectionMessage(voteUsers, collection.getDarCode());
1✔
691
        } else {
692
          emailService.sendDarNewCollectionElectionMessage(voteUsers, collection.getDarCode());
1✔
693
        }
694

695
      } catch (Exception e) {
1✔
696
        logException(
1✔
697
            "Unable to send new case message to DAC members for DAR Collection: %s".formatted(
1✔
698
                collection.getDarCode()), e);
1✔
699
      }
1✔
700
    } catch (Exception e) {
1✔
701
      logException("Exception creating elections and votes for collection: %s".formatted(
1✔
702
          collection.getDarCollectionId()), e);
1✔
703
      throw e;
1✔
704
    }
1✔
705
    return darCollectionDAO.findDARCollectionByCollectionId(collection.getDarCollectionId());
1✔
706
  }
707

708
  // Private helper method to mark Elections as 'Canceled'
709
  private void cancelElectionsForReferenceIds(List<String> referenceIds) {
710
    List<Election> elections = electionDAO.findOpenElectionsByReferenceIds(referenceIds);
1✔
711
    elections.forEach(election -> {
1✔
712
      if (!election.getStatus().equals(ElectionStatus.CANCELED.getValue())) {
1✔
713
        electionDAO.updateElectionById(election.getElectionId(), ElectionStatus.CANCELED.getValue(),
1✔
714
            new Date());
715
      }
716
    });
1✔
717
  }
1✔
718

719

720
  public void sendNewDARCollectionMessage(Integer collectionId)
721
      throws IOException, TemplateException {
722
    DarCollection collection = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
723
    if (collection == null) {
1✔
NEW
724
      logWarn(
×
725
          "Sending new DAR Collection message: Could not find collection for specified collection id: "
726
              + collectionId);
NEW
727
      return;
×
728
    }
729
    // Do this, but only for a single DAR
730
    DataAccessRequest dar = collection.getMostRecentDar();
1✔
731
    List<User> distinctUsers = getDistinctAdminAndChairUsersForDAR(dar);
1✔
732
    User researcher = userDAO.findUserById(collection.getCreateUserId());
1✔
733
    if (researcher == null) {
1✔
NEW
734
      logWarn(
×
735
          "Sending new DAR Collection message: Could not find researcher for specified user id: "
NEW
736
              + collection.getCreateUserId());
×
737
    }
738
    String researcherName = researcher == null ? "Unknown" : researcher.getDisplayName();
1✔
739
    // Only do this for the DAR... dacDAO.findDacsForDatasetIds(dar.getDatasetIds())
740
    Collection<Dac> dacsInDAR = dacDAO.findDacsForDatasetIds(dar.getDatasetIds());
1✔
741
    // Use only the datasets from the dar
742
    List<Integer> datasetIds = dar.getDatasetIds();
1✔
743
    List<Dataset> datasetsInDAR =
744
        datasetIds.isEmpty() ? List.of() : datasetDAO.findDatasetsByIdList(datasetIds);
1✔
745

746
    Map<String, List<String>> sendList = new HashMap<>();
1✔
747
    for (User user : distinctUsers) {
1✔
748
      List<Dac> matchingDacsForUser = getMatchingDacs(user, dacsInDAR);
1✔
749
      for (Dac dac : matchingDacsForUser) {
1✔
750
        List<String> matchingDatasetsForDac = getMatchingDatasets(dac, datasetsInDAR);
1✔
751
        if (matchingDatasetsForDac != null) {
1✔
752
          sendList.put(dac.getName(), matchingDatasetsForDac);
1✔
753
        }
754
      }
1✔
755
      // If the dar is not a progress report, use the DAR template else use the PR template.
756
      if (dar.getProgressReport()) {
1✔
757
        // Use the reference ID to link the fact that this progress report will have been noted.
758
        // the DAR Code at this point will be ambiguous.
NEW
759
        emailService.sendNewProgressReportRequestEmail(user, sendList, researcherName, collection.getDarCode(), dar.getReferenceId());
×
760
      } else {
761
        emailService.sendNewDARRequestEmail(user, sendList, researcherName, collection.getDarCode());
1✔
762
      }
763
    }
1✔
764
  }
1✔
765

766
  private List<User> getDistinctAdminAndChairUsersForDAR(DataAccessRequest dar) {
767
    List<Integer> datasetIds = dar.getDatasetIds();
1✔
768
    return getDistinctAdminAndChairUsersForDatasetIds(datasetIds);
1✔
769
  }
770

771
  private List<User> getDistinctAdminAndChairUsersForDatasetIds(List<Integer> datasetIds) {
772
    List<User> admins = userDAO.describeUsersByRoleAndEmailPreference(UserRoles.ADMIN.getRoleName(),
1✔
773
        true);
1✔
774
    Set<User> chairPersons = userDAO.findUsersForDatasetsByRole(datasetIds,
1✔
775
        Collections.singletonList(UserRoles.CHAIRPERSON.getRoleName()));
1✔
776
    // Ensure that admins/chairs are not double emailed
777
    // and filter users that don't want to receive email
778
    return Streams.concat(admins.stream(), chairPersons.stream())
1✔
779
        .filter(u -> Boolean.TRUE.equals(u.getEmailPreference()))
1✔
780
        .distinct()
1✔
781
        .toList();
1✔
782
  }
783

784
  private List<Dac> getMatchingDacs(User user, Collection<Dac> dacsInDAR) {
785
    List<Integer> dacIDs = user.getRoles().stream()
1✔
786
        .map(UserRole::getDacId)
1✔
787
        .filter(Objects::nonNull)
1✔
788
        .toList();
1✔
789
    return dacsInDAR.stream()
1✔
790
        .filter(dac -> dacIDs.contains(dac.getDacId()))
1✔
791
        .toList();
1✔
792
  }
793

794
  private List<String> getMatchingDatasets(Dac dac, List<Dataset> datasetsInDAR) {
795
    return datasetsInDAR.stream()
1✔
796
        .filter(dataset -> dataset.getDacId().equals(dac.getDacId()))
1✔
797
        .map(Dataset::getDatasetIdentifier)
1✔
798
        .toList();
1✔
799
  }
800

801
}
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

© 2026 Coveralls, Inc