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

DataBiosphere / consent / #5907

14 May 2025 09:15PM UTC coverage: 78.701% (+0.003%) from 78.698%
#5907

push

web-flow
DT-1616: Disallow Cancel for submitted progress reports (#2524)

41 of 44 new or added lines in 3 files covered. (93.18%)

10036 of 12752 relevant lines covered (78.7%)

0.79 hits per line

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

92.12
/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.gson.Gson;
7
import com.google.inject.Inject;
8
import jakarta.ws.rs.BadRequestException;
9
import jakarta.ws.rs.NotAcceptableException;
10
import jakarta.ws.rs.NotAuthorizedException;
11
import jakarta.ws.rs.NotFoundException;
12
import java.text.SimpleDateFormat;
13
import java.util.ArrayList;
14
import java.util.Collection;
15
import java.util.Date;
16
import java.util.HashMap;
17
import java.util.List;
18
import java.util.Map;
19
import java.util.Objects;
20
import java.util.Set;
21
import java.util.function.Function;
22
import java.util.function.Predicate;
23
import java.util.stream.Collectors;
24
import java.util.stream.Stream;
25
import org.broadinstitute.consent.http.db.DarCollectionDAO;
26
import org.broadinstitute.consent.http.db.DarCollectionSummaryDAO;
27
import org.broadinstitute.consent.http.db.DataAccessRequestDAO;
28
import org.broadinstitute.consent.http.db.DatasetDAO;
29
import org.broadinstitute.consent.http.db.ElectionDAO;
30
import org.broadinstitute.consent.http.db.MatchDAO;
31
import org.broadinstitute.consent.http.db.VoteDAO;
32
import org.broadinstitute.consent.http.enumeration.DarCollectionActions;
33
import org.broadinstitute.consent.http.enumeration.DarCollectionStatus;
34
import org.broadinstitute.consent.http.enumeration.DarStatus;
35
import org.broadinstitute.consent.http.enumeration.ElectionStatus;
36
import org.broadinstitute.consent.http.enumeration.UserRoles;
37
import org.broadinstitute.consent.http.enumeration.VoteType;
38
import org.broadinstitute.consent.http.models.DarCollection;
39
import org.broadinstitute.consent.http.models.DarCollectionSummary;
40
import org.broadinstitute.consent.http.models.DataAccessRequest;
41
import org.broadinstitute.consent.http.models.DataAccessRequestData;
42
import org.broadinstitute.consent.http.models.Dataset;
43
import org.broadinstitute.consent.http.models.Election;
44
import org.broadinstitute.consent.http.models.User;
45
import org.broadinstitute.consent.http.models.UserRole;
46
import org.broadinstitute.consent.http.models.Vote;
47
import org.broadinstitute.consent.http.service.dao.DarCollectionServiceDAO;
48
import org.broadinstitute.consent.http.util.ConsentLogger;
49

50
public class DarCollectionService implements ConsentLogger {
51

52
  private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
1✔
53
  private final DarCollectionDAO darCollectionDAO;
54
  private final DarCollectionServiceDAO collectionServiceDAO;
55
  private final DarCollectionSummaryDAO darCollectionSummaryDAO;
56
  private final DataAccessRequestDAO dataAccessRequestDAO;
57
  private final DatasetDAO datasetDAO;
58
  private final ElectionDAO electionDAO;
59
  private final VoteDAO voteDAO;
60
  private final MatchDAO matchDAO;
61
  private final EmailService emailService;
62

63
  @Inject
64
  public DarCollectionService(DarCollectionDAO darCollectionDAO,
65
      DarCollectionServiceDAO collectionServiceDAO, DatasetDAO datasetDAO, ElectionDAO electionDAO,
66
      DataAccessRequestDAO dataAccessRequestDAO, EmailService emailService, VoteDAO voteDAO,
67
      MatchDAO matchDAO, DarCollectionSummaryDAO darCollectionSummaryDAO) {
1✔
68
    this.darCollectionDAO = darCollectionDAO;
1✔
69
    this.collectionServiceDAO = collectionServiceDAO;
1✔
70
    this.datasetDAO = datasetDAO;
1✔
71
    this.electionDAO = electionDAO;
1✔
72
    this.dataAccessRequestDAO = dataAccessRequestDAO;
1✔
73
    this.emailService = emailService;
1✔
74
    this.voteDAO = voteDAO;
1✔
75
    this.matchDAO = matchDAO;
1✔
76
    this.darCollectionSummaryDAO = darCollectionSummaryDAO;
1✔
77
  }
1✔
78

79
  private void updateStatusCount(Map<String, Integer> statusCount, String status) {
80
    // If the status is null, track it as Undefined to ensure election is accounted for.
81
    statusCount.merge(Objects.requireNonNullElse(status, "Undefined"), 1, Integer::sum);
1✔
82
  }
1✔
83

84
  private void determineCollectionStatus(DarCollectionSummary summary,
85
      Map<String, Integer> statusCount, Integer datasetCount, Integer electionCount) {
86
    //If there are no elections, status is unreviewed
87
    //if there are some elections open, status is in process
88
    //if all elections are closed or canceled and electionCount == datasetCount, status is complete
89
    if (electionCount.equals(0)) {
1✔
90
      summary.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
91
    } else if (electionCount.equals(datasetCount)) {
1✔
92
      Integer openCount = statusCount.get(ElectionStatus.OPEN.getValue());
1✔
93
      if (Objects.isNull(openCount)) {
1✔
94
        summary.setStatus(DarCollectionStatus.COMPLETE.getValue());
1✔
95
      } else {
96
        summary.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
97
      }
98
    } else {
1✔
99
      summary.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
100
    }
101
  }
1✔
102

103
  private void processDarCollectionSummariesForAdmin(List<DarCollectionSummary> summaries) {
104
    //if at least one election is open, show cancel
105
    //if at least one non-open/absent election, show open
106
    summaries.forEach(s -> {
1✔
107
      Map<String, Integer> statusCount = new HashMap<>();
1✔
108
      Map<Integer, Election> elections = s.getElections();
1✔
109
      if (elections.isEmpty()) {
1✔
110
        s.addAction(DarCollectionActions.OPEN);
1✔
111
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
112
      } else {
113
        elections.values().forEach(e -> {
1✔
114
          String status = e.getStatus();
1✔
115
          updateStatusCount(statusCount, status);
1✔
116
          if (status.equals(ElectionStatus.OPEN.getValue())) {
1✔
117
            s.addAction(DarCollectionActions.CANCEL);
1✔
118
          } else {
119
            s.addAction(DarCollectionActions.OPEN);
1✔
120
          }
121
        });
1✔
122
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
123
      }
124
    });
1✔
125
  }
1✔
126

127
  private DarCollectionSummary processDraftAsSummary(DataAccessRequest d) {
128
    try {
129
      DarCollectionSummary summary = new DarCollectionSummary();
1✔
130
      String darCode = "DRAFT_DAR_" + sdf.format(d.getCreateDate());
1✔
131
      summary.setDarCode(darCode);
1✔
132
      summary.setStatus(DarCollectionStatus.DRAFT.getValue());
1✔
133
      summary.setName(d.getData().getProjectTitle());
1✔
134
      summary.addAction(DarCollectionActions.RESUME);
1✔
135
      summary.addAction(DarCollectionActions.DELETE);
1✔
136
      summary.addReferenceId(d.referenceId);
1✔
137
      return summary;
1✔
138
    } catch (Exception e) {
×
139
      logWarn("Error processing draft with id: %s".formatted(d.getId()), e);
×
140
    }
141
    return null;
×
142
  }
143

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

179
  private void processDarCollectionSummariesForMember(List<DarCollectionSummary> summaries,
180
      Integer userId) {
181
    summaries.forEach(s -> {
1✔
182
      Collection<Election> elections = s.getElections().values();
1✔
183
      Integer electionCount = elections.size();
1✔
184
      //if there are no elections present, unreviewed
185
      //if there are elections present. in process
186
      if (electionCount == 0) {
1✔
187
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
188
      } else {
189
        boolean isVotable = elections
1✔
190
            .stream()
1✔
191
            .anyMatch(
1✔
192
                election -> election.getStatus().equalsIgnoreCase(ElectionStatus.OPEN.getValue()));
1✔
193

194
        if (isVotable) {
1✔
195
          s.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
1✔
196
          List<Vote> votes = s.getVotes().stream()
1✔
197
              .filter(
1✔
198
                  v -> v.getUserId().equals(userId) && v.getType().equals(VoteType.DAC.getValue()))
1✔
199
              .toList();
1✔
200
          if (!votes.isEmpty()) {
1✔
201
            boolean hasVoted = votes.stream().map(Vote::getVote).allMatch(Objects::nonNull);
1✔
202
            DarCollectionActions targetAction = hasVoted ? DarCollectionActions.UPDATE
1✔
203
                : DarCollectionActions.VOTE;
1✔
204
            s.addAction(targetAction);
1✔
205
          }
206
        } else {
1✔
207
          //non-votable states
208
          //all canceled (complete)
209
          //some datasets do not have elections (in process)
210
          //all voted on (complete)
211
          //no elections
212
          if (electionCount < s.getDatasetCount()) {
1✔
213
            s.setStatus(DarCollectionStatus.IN_PROCESS.getValue());
×
214
          } else {
215
            s.setStatus(DarCollectionStatus.COMPLETE.getValue());
1✔
216
          }
217
        }
218
      }
219
    });
1✔
220
  }
1✔
221

222

223
  private void processDarCollectionSummariesForChair(List<DarCollectionSummary> summaries) {
224
    summaries.forEach(s -> {
1✔
225
      //if there are no elections, only show open
226
      //if there is any closed or canceled elections, or if some datasets dont have an election, show open
227
      //if there are any open elections, show cancel and vote
228
      Map<String, Integer> statusCount = new HashMap<>();
1✔
229
      Map<Integer, Election> elections = s.getElections();
1✔
230
      if (elections.size() == 0) {
1✔
231
        s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
1✔
232
        s.addAction(DarCollectionActions.OPEN);
1✔
233
      } else {
234
        if (elections.size() < s.getDatasetCount()) {
1✔
235
          s.addAction(DarCollectionActions.OPEN);
1✔
236
        }
237
        elections.values().forEach(election -> {
1✔
238
          String statusString = election.getStatus();
1✔
239
          updateStatusCount(statusCount, statusString);
1✔
240
          ElectionStatus status = ElectionStatus.getStatusFromString(statusString);
1✔
241
          switch (status) {
1✔
242
            case CLOSED, CANCELED:
243
              s.addAction(DarCollectionActions.OPEN);
1✔
244
              break;
1✔
245
            case OPEN:
246
              s.addAction(DarCollectionActions.VOTE);
1✔
247
              break;
1✔
248
            default:
249
              break;
250
          }
251
        });
1✔
252
        Integer closedCount = statusCount.get(ElectionStatus.CLOSED.getValue());
1✔
253
        Integer openCount = statusCount.get(ElectionStatus.OPEN.getValue());
1✔
254
        //add cancel if there are no closed elections and at least one open election
255
        if (Objects.isNull(closedCount) && Objects.nonNull(openCount)) {
1✔
256
          s.addAction(DarCollectionActions.CANCEL);
1✔
257
        }
258

259
        determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
260
      }
261
    });
1✔
262
  }
1✔
263

264
  private void processDarCollectionSummariesForSO(List<DarCollectionSummary> summaries) {
265
    summaries.forEach(s -> {
1✔
266
      Map<String, Integer> statusCount = new HashMap<>();
1✔
267
      s.getElections().values()
1✔
268
          .forEach(election -> updateStatusCount(statusCount, election.getStatus()));
1✔
269
      determineCollectionStatus(s, statusCount, s.getDatasetCount(), s.getElections().size());
1✔
270
    });
1✔
271
  }
1✔
272

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

322
  private List<Integer> getDatasetIdsForUserAndRoleId(User user, Integer roleId) {
323
    List<Integer> roleDacIds = user.getRoles().stream()
1✔
324
        .filter(ur -> Objects.nonNull(ur.getRoleId()))
1✔
325
        .filter(ur -> ur.getRoleId().equals(roleId))
1✔
326
        .map(UserRole::getDacId)
1✔
327
        .filter(Objects::nonNull)
1✔
328
        .toList();
1✔
329
    return Stream.of(roleDacIds)
1✔
330
        .filter(Predicate.not(List::isEmpty))
1✔
331
        .map(datasetDAO::findDatasetListByDacIds)
1✔
332
        .flatMap(List::stream)
1✔
333
        .map(Dataset::getDatasetId)
1✔
334
        .toList();
1✔
335
  }
336

337
  /**
338
   * Finds the DarCollectionSummary for a given darCollectionId, processed by the given role.
339
   *
340
   * @param user         The user making the request
341
   * @param role         The role the user is making the request as
342
   * @param collectionId The darCollectionId of the requested DarCollectionSummary
343
   * @return A DarCollectionSummary object
344
   */
345
  public DarCollectionSummary getSummaryForRoleByCollectionId(User user, UserRoles role,
346
      Integer collectionId) {
347
    DarCollectionSummary summary = null;
1✔
348
    Integer userId = user.getUserId();
1✔
349
    List<Integer> datasetIds;
350
    try {
351
      switch (role) {
1✔
352
        case ADMIN:
353
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
354
          processDarCollectionSummariesForAdmin(List.of(summary));
1✔
355
          break;
1✔
356
        case SIGNINGOFFICIAL:
357
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
358
          processDarCollectionSummariesForSO(List.of(summary));
1✔
359
          break;
1✔
360
        case CHAIRPERSON:
361
          datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.CHAIRPERSON.getRoleId());
1✔
362
          summary = darCollectionSummaryDAO.getDarCollectionSummaryForDACByCollectionId(userId,
1✔
363
              datasetIds, collectionId);
364
          processDarCollectionSummariesForChair(List.of(summary));
1✔
365
          break;
1✔
366
        case MEMBER:
367
          datasetIds = getDatasetIdsForUserAndRoleId(user, UserRoles.MEMBER.getRoleId());
1✔
368
          summary = darCollectionSummaryDAO.getDarCollectionSummaryForDACByCollectionId(userId,
1✔
369
              datasetIds, collectionId);
370
          processDarCollectionSummariesForMember(List.of(summary), userId);
1✔
371
          break;
1✔
372
        case RESEARCHER:
373
          summary = darCollectionSummaryDAO.getDarCollectionSummaryByCollectionId(collectionId);
1✔
374
          processDarCollectionSummariesForResearcher(List.of(summary));
1✔
375
          break;
1✔
376
        default:
377
          break;
378
      }
379
      return summary;
1✔
380
    } catch (NullPointerException e) {
1✔
381
      throw new NotFoundException(
1✔
382
          "Collection summary with the collection id of " + collectionId + " was not found");
383
    }
384
  }
385

386
  public DarCollectionSummary updateCollectionToDraftStatus(DarCollection sourceCollection) {
387
    sourceCollection.getDars().values().forEach((d) -> {
×
388
      Date now = new Date();
×
389
      DataAccessRequestData newData = new Gson().fromJson(d.getData().toString(),
×
390
          DataAccessRequestData.class);
391
      newData.setDarCode(null);
×
392
      newData.setStatus(null);
×
393
      newData.setReferenceId(d.getReferenceId());
×
394
      newData.setSortDate(now.getTime());
×
395
      dataAccessRequestDAO.updateDataByReferenceId(
×
396
          d.getReferenceId(),
×
397
          d.getUserId(),
×
398
          now,
399
          null,
400
          now,
401
          newData,
402
          null
403
      );
404
    });
×
405

406
    // get updated collection
407
    sourceCollection = this.darCollectionDAO.findDARCollectionByCollectionId(
×
408
        sourceCollection.getDarCollectionId());
×
409

410
    return this.processDraftAsSummary(new ArrayList<>(sourceCollection.getDars().values()).get(0));
×
411
  }
412

413
  /**
414
   * Find all dataset ids by the DAC User. Will return ids for Chairpersons or Members
415
   *
416
   * @param user The DAC User
417
   * @return List of Dataset IDs
418
   */
419
  public List<Integer> findDatasetIdsByDACUser(User user) {
420
    return datasetDAO.findDatasetIdsByDACUserId(user.getUserId());
×
421
  }
422

423
  public void deleteByCollectionId(User user, Integer collectionId)
424
      throws NotAcceptableException, NotAuthorizedException, NotFoundException {
425
    DarCollection coll = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
426
    if (coll == null) {
1✔
427
      throw new NotFoundException("DAR Collection does not exist at that id.");
1✔
428
    }
429

430
    // ensure the user is capable of deleting the collection
431
    if (!user.hasUserRole(UserRoles.ADMIN) && !coll.getCreateUserId().equals(user.getUserId())) {
1✔
432
      throw new NotAuthorizedException("Not authorized to delete DAR Collection.");
1✔
433
    }
434

435
    // get the reference ids of the dars in the collection
436
    List<String> referenceIds =
1✔
437
        coll.getDars().values().stream().map(DataAccessRequest::getReferenceId).distinct()
1✔
438
            .collect(toList());
1✔
439

440
    // ensure there are no elections; if there are, will attempt to delete (must be admin)
441
    ensureNoElections(user, referenceIds);
1✔
442

443
    // no elections left & user has perms => safe to delete collection
444

445
    // delete DARs
446
    matchDAO.deleteRationalesByPurposeIds(referenceIds);
1✔
447
    matchDAO.deleteMatchesByPurposeIds(referenceIds);
1✔
448
    dataAccessRequestDAO.deleteDARDatasetRelationByReferenceIds(referenceIds);
1✔
449
    dataAccessRequestDAO.deleteByReferenceIds(referenceIds);
1✔
450

451
    // delete collection
452
    darCollectionDAO.deleteByCollectionId(collectionId);
1✔
453
  }
1✔
454

455
  // checks if there are any elections for any of the DARs in the referenceIds; if so,
456
  // will attempt to delete them (must be admin to delete)
457
  private void ensureNoElections(User user, List<String> referenceIds)
458
      throws NotAcceptableException {
459
    // get elections across all reference ids
460
    List<Election> allElections = electionDAO.findElectionsByReferenceIds(referenceIds);
1✔
461

462
    // if there are already no elections, we're done!
463
    if (allElections.isEmpty()) {
1✔
464
      return;
1✔
465
    }
466

467
    // if there are any elections, we need to delete them.
468
    // only admins can delete elections; make sure user is an admin
469
    if (!user.hasUserRole(UserRoles.ADMIN)) {
1✔
470
      throw new NotAcceptableException("Cannot delete DAR with elections.");
1✔
471
    }
472

473
    // delete all votes
474
    voteDAO.deleteVotesByReferenceIds(referenceIds);
1✔
475

476
    // delete all elections
477
    List<Integer> electionIds = allElections.stream().map(Election::getElectionId)
1✔
478
        .collect(toList());
1✔
479

480
    electionDAO.deleteElectionsByIds(electionIds);
1✔
481

482
  }
1✔
483

484
  public DarCollection getByReferenceId(String referenceId) {
485
    DarCollection collection = darCollectionDAO.findDARCollectionByReferenceId(referenceId);
×
486
    if (Objects.isNull(collection)) {
×
487
      throw new NotFoundException(
×
488
          "Collection with the reference id of " + referenceId + " was not found");
489
    }
NEW
490
    return addDatasetsToCollection(collection);
×
491
  }
492

493
  public DarCollection getByCollectionId(Integer collectionId) {
494
    DarCollection collection = darCollectionDAO.findDARCollectionByCollectionId(collectionId);
1✔
495
    if (Objects.isNull(collection)) {
1✔
496
      throw new NotFoundException(
×
497
          "Collection with the collection id of " + collectionId + " was not found");
498
    }
499
    return addDatasetsToCollection(collection);
1✔
500
  }
501

502
  /**
503
   * Given a DarCollection, add its relevant datasets.
504
   *
505
   * @param collection      The list of DarCollections to iterate over.
506
   * @return collection with datasets added
507
   */
508
  @VisibleForTesting
509
  protected DarCollection addDatasetsToCollection(DarCollection collection) {
510
    // get datasetIds from each DAR from each collection
511
    List<String> referenceIds = List.copyOf(collection.getDars().keySet());
1✔
512
    List<Integer> datasetIds = referenceIds.isEmpty() ? List.of()
1✔
513
        : dataAccessRequestDAO.findAllDARDatasetRelations(referenceIds);
1✔
514
    if (!datasetIds.isEmpty()) {
1✔
515
      Map<Integer, Dataset> datasetMap = datasetDAO.findDatasetsByIdList(datasetIds)
1✔
516
          .stream()
1✔
517
          .distinct()
1✔
518
          .collect(Collectors.toMap(Dataset::getDatasetId, Function.identity()));
1✔
519

520
        Set<Dataset> collectionDatasets = collection.getDars().values().stream()
1✔
521
            .map(DataAccessRequest::getDatasetIds)
1✔
522
            .flatMap(Collection::stream)
1✔
523
            .map(datasetMap::get)
1✔
524
            .filter(Objects::nonNull) // filtering out nulls which were getting captured by map
1✔
525
            .collect(Collectors.toSet());
1✔
526
        DarCollection copy = collection.deepCopy();
1✔
527
        copy.setDatasets(collectionDatasets);
1✔
528
        return copy;
1✔
529
    }
530
    // There were no datasets to add, so we return the original list
531
    return collection;
1✔
532
  }
533

534
  /**
535
   * Cancel Elections or a dar for a DarCollection, given a user and a role. If the user is a chair,
536
   * or admin, cancel elections. If the user is a researcher, cancel the dar.
537
   *
538
   * @param user       The User initiating the cancel
539
   * @param collection The DarCollection
540
   * @param role       The role of the user, must be one of ADMIN, CHAIRPERSON, or RESEARCHER
541
   * @return The DarCollection that has been canceled
542
   */
543
  public DarCollection cancelDarCollectionByRole(User user, DarCollection collection, UserRoles role) {
544
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
545
    if (dars.isEmpty()) {
1✔
546
      logWarn("DAR Collection ID: [%s] does not have any associated DAR ids".formatted(
1✔
547
          collection.getDarCollectionId()));
1✔
548
      return collection;
1✔
549
    }
550

551
    return switch (role) {
1✔
552
      case ADMIN -> cancelDarCollectionElectionsAsAdmin(collection);
1✔
553
      case CHAIRPERSON ->
554
          cancelDarCollectionElectionsAsChair(collection, user);
1✔
555
      default -> cancelDarCollectionAsResearcher(collection, user);
1✔
556
    };
557
  }
558

559
  /**
560
   * Cancel a DarCollection as a researcher.
561
   * <p>
562
   * If an election exists for a DAR within the collection, that DAR cannot be cancelled by the
563
   * researcher. Since it's now under DAC review, it's up to the DAC Chair (or admin) to ultimately
564
   * decline or cancel the elections for the collection.
565
   *
566
   * @param collection The DarCollection
567
   * @param user the researcher requesting the cancel
568
   * @return The canceled DarCollection
569
   */
570
  private DarCollection cancelDarCollectionAsResearcher(DarCollection collection, User user) {
571
    if (!user.getUserId().equals(collection.getCreateUserId())) {
1✔
NEW
572
      throw new NotFoundException();
×
573
    }
574
    DarCollectionSummary summary = darCollectionSummaryDAO
1✔
575
        .getDarCollectionSummaryByCollectionId(collection.getDarCollectionId());
1✔
576
    if (summary.getProgressReport()) {
1✔
577
      throw new BadRequestException("Cannot cancel a progress report");
1✔
578
    }
579

580
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
581
    List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();
1✔
582

583
    List<Election> elections = electionDAO.findLastElectionsByReferenceIds(referenceIds);
1✔
584
    if (!elections.isEmpty()) {
1✔
585
      throw new BadRequestException("Elections present on DARs; cannot cancel collection");
1✔
586
    }
587

588
    // Cancel active dars for the researcher
589
    List<String> activeDarIds = dars.stream()
1✔
590
        .filter(d -> !DataAccessRequest.isCanceled(d))
1✔
591
        .map(DataAccessRequest::getReferenceId)
1✔
592
        .toList();
1✔
593
    if (!activeDarIds.isEmpty()) {
1✔
594
      dataAccessRequestDAO.cancelByReferenceIds(activeDarIds);
1✔
595
    }
596

597
    return getByCollectionId(collection.getDarCollectionId());
1✔
598
  }
599

600
  /**
601
   * Cancel Elections for a DarCollection as an admin.
602
   * <p>
603
   * Admins can cancel all elections in a DarCollection
604
   *
605
   * @param collection The DarCollection
606
   * @return The DarCollection whose elections have been canceled
607
   */
608
  private DarCollection cancelDarCollectionElectionsAsAdmin(DarCollection collection) {
609
    Collection<DataAccessRequest> dars = collection.getDars().values();
1✔
610
    List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();
1✔
611

612
    // Cancel all DAR elections
613
    cancelElectionsForReferenceIds(referenceIds);
1✔
614

615
    return getByCollectionId(collection.getDarCollectionId());
1✔
616
  }
617

618
  /**
619
   * Cancel Elections for a DarCollection as a chairperson.
620
   * <p>
621
   * Chairs can only cancel Elections that reference a dataset the chair is a DAC member for.
622
   *
623
   * @param collection The DarCollection
624
   * @return The DarCollection whose elections have been canceled
625
   */
626
  private DarCollection cancelDarCollectionElectionsAsChair(DarCollection collection, User user) {
627
    // Find dataset ids the chairperson has access to:
628
    Set<Integer> datasetIds = Set.copyOf(datasetDAO.findDatasetIdsByDACUserId(user.getUserId()));
1✔
629

630
    // Filter the list of DARs we can operate on by the datasets accessible to this chairperson
631
    List<String> referenceIds = collection.getDars().values().stream()
1✔
632
        .filter(d -> datasetIds.containsAll(d.getDatasetIds()))
1✔
633
        .map(DataAccessRequest::getReferenceId)
1✔
634
        .toList();
1✔
635

636
    if (referenceIds.isEmpty()) {
1✔
637
      logWarn(
1✔
638
          "DAR Collection ID: [%s] does not have any associated DARs that this chairperson can access".formatted(
1✔
639
              collection.getDarCollectionId()));
1✔
640
      return collection;
1✔
641
    }
642

643
    // Cancel filtered DAR elections
644
    cancelElectionsForReferenceIds(referenceIds);
1✔
645

646
    return getByCollectionId(collection.getDarCollectionId());
1✔
647
  }
648

649
  /**
650
   * DarCollections with no elections, or with previously canceled elections, are valid for
651
   * initiating a new set of elections. Elections in open, closed, pending, or final states are not
652
   * valid.
653
   *
654
   * @param user       The User initiating new elections for a collection
655
   * @param collection The DarCollection
656
   * @return The updated DarCollection
657
   */
658
  public DarCollection createElectionsForDarCollection(User user, DarCollection collection)
659
      throws Exception {
660
    try {
661
      List<String> createdElectionReferenceIds = collectionServiceDAO.createElectionsForDarCollection(
1✔
662
          user, collection);
663
      if (createdElectionReferenceIds.isEmpty()) {
1✔
664
        var e = new IllegalStateException(
1✔
665
            "No elections were created for DAR Collection: %s".formatted(
1✔
666
                collection.getDarCode()));
1✔
667
        logException(e);
1✔
668
        throw e;
1✔
669
      }
670
      try {
671
        List<User> voteUsers = voteDAO.findVoteUsersByElectionReferenceIdList(
1✔
672
            createdElectionReferenceIds);
673
        emailService.sendDarNewCollectionElectionMessage(voteUsers, collection);
1✔
674
      } catch (Exception e) {
1✔
675
        logException(
1✔
676
            "Unable to send new case message to DAC members for DAR Collection: %s".formatted(
1✔
677
                collection.getDarCode()), e);
1✔
678
      }
1✔
679
    } catch (Exception e) {
1✔
680
      logException("Exception creating elections and votes for collection: %s".formatted(
1✔
681
          collection.getDarCollectionId()), e);
1✔
682
      throw e;
1✔
683
    }
1✔
684
    return darCollectionDAO.findDARCollectionByCollectionId(collection.getDarCollectionId());
1✔
685
  }
686

687
  // Private helper method to mark Elections as 'Canceled'
688
  private void cancelElectionsForReferenceIds(List<String> referenceIds) {
689
    List<Election> elections = electionDAO.findOpenElectionsByReferenceIds(referenceIds);
1✔
690
    elections.forEach(election -> {
1✔
691
      if (!election.getStatus().equals(ElectionStatus.CANCELED.getValue())) {
1✔
692
        electionDAO.updateElectionById(election.getElectionId(), ElectionStatus.CANCELED.getValue(),
1✔
693
            new Date());
694
      }
695
    });
1✔
696
  }
1✔
697
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc