diff --git a/README.md b/README.md index 070b7b6ba5..db1e2a9dfa 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ OpenEx is an open source platform allowing organizations to plan, schedule and c The goal is to create a powerful, reliable and open source tool to effectively plan and play all types of training, exercises and simulation from the technical level to the strategic one. The need for rationalization and capitalization from one year to the next, as well as the publication of ISO 22398: 2013 standard necessarily lead to the need to acquire specific software. -OpenEx aims to respond to these issues, which not only concern state services but also many private organizations. With different modules (scenarios, audiences, simulations, verification of means of communication, encryption, etc.), the platform offers advantages such as collaborative work, real-time monitoring, statistics or the management of feedback. +OpenEx aims to respond to these issues, which not only concern state services but also many private organizations. With different modules (scenarios, teams, simulations, verification of means of communication, encryption, etc.), the platform offers advantages such as collaborative work, real-time monitoring, statistics or the management of feedback. Finally, OpenEx supports different types of inject, allowing the tool to be integrated with emails, SMS platforms, social medias, alarm systems, etc. All currently supported integration can be found in the [OpenEx ecosystem](https://filigran.notion.site/OpenEx-Ecosystem-30d8eb73d7d04611843e758ddef8941b). diff --git a/openex-api/src/main/java/io/openex/helper/InjectHelper.java b/openex-api/src/main/java/io/openex/helper/InjectHelper.java index 239b20c039..e29eb4ed5a 100644 --- a/openex-api/src/main/java/io/openex/helper/InjectHelper.java +++ b/openex-api/src/main/java/io/openex/helper/InjectHelper.java @@ -2,7 +2,7 @@ import io.openex.contract.Contract; import io.openex.database.model.*; -import io.openex.database.repository.AudienceRepository; +import io.openex.database.repository.TeamRepository; import io.openex.database.repository.DryInjectRepository; import io.openex.database.repository.InjectRepository; import io.openex.database.specification.DryInjectSpecification; @@ -24,7 +24,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static io.openex.database.specification.AudienceSpecification.fromExercise; import static io.openex.helper.StreamHelper.fromIterable; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Stream.concat; @@ -32,98 +31,102 @@ @Component public class InjectHelper { - private InjectRepository injectRepository; - private DryInjectRepository dryInjectRepository; - private AudienceRepository audienceRepository; - private ContractService contractService; - private ExecutionContextService executionContextService; + private InjectRepository injectRepository; + private DryInjectRepository dryInjectRepository; + private TeamRepository teamRepository; + private ContractService contractService; + private ExecutionContextService executionContextService; - @Autowired - public void setContractService(ContractService contractService) { - this.contractService = contractService; - } + @Autowired + public void setContractService(ContractService contractService) { + this.contractService = contractService; + } - @Autowired - public void setAudienceRepository(AudienceRepository audienceRepository) { - this.audienceRepository = audienceRepository; - } + @Autowired + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; + } - @Autowired - public void setInjectRepository(InjectRepository injectRepository) { - this.injectRepository = injectRepository; - } + @Autowired + public void setInjectRepository(InjectRepository injectRepository) { + this.injectRepository = injectRepository; + } - @Autowired - public void setDryInjectRepository(DryInjectRepository dryInjectRepository) { - this.dryInjectRepository = dryInjectRepository; - } + @Autowired + public void setDryInjectRepository(DryInjectRepository dryInjectRepository) { + this.dryInjectRepository = dryInjectRepository; + } - @Autowired - public void setExecutionContextService(@NotNull final ExecutionContextService executionContextService) { - this.executionContextService = executionContextService; - } + @Autowired + public void setExecutionContextService(@NotNull final ExecutionContextService executionContextService) { + this.executionContextService = executionContextService; + } - private List getInjectAudiences(Inject inject) { - Exercise exercise = inject.getExercise(); - return inject.isAllAudiences() ? - fromIterable(this.audienceRepository.findAll(fromExercise(exercise.getId()))) : inject.getAudiences(); - } + private List getInjectTeams(Inject inject) { + Exercise exercise = inject.getExercise(); + return inject.isAllTeams() ? exercise.getTeams() : inject.getTeams(); + } - private Stream> getUsersFromInjection(Injection injection) { - if (injection instanceof DryInject dryInject) { - return dryInject.getRun().getUsers().stream() - .map(user -> Tuples.of(user, "Dryrun")); - } else if (injection instanceof Inject inject) { - List audiences = getInjectAudiences(inject); - return audiences.stream().filter(Audience::isEnabled) - .flatMap(audience -> audience.getUsers().stream() - .map(user -> Tuples.of(user, audience.getName()))); + private Stream> getUsersFromInjection(Injection injection) { + if (injection instanceof DryInject dryInject) { + return dryInject.getRun().getUsers().stream() + .map(user -> Tuples.of(user, "Dryrun")); + } else if (injection instanceof Inject inject) { + List teams = getInjectTeams(inject); + // We get all the teams for this inject + // But those team can be used in other exercises with different players enabled + // So we need to focus on team players only enabled in the context of the current exercise + return teams.stream().flatMap(team -> + team.getExerciseTeamUsers().stream() + .filter(exerciseTeamUser -> exerciseTeamUser.getExercise().getId().equals(injection.getExercise().getId())) + .map(exerciseTeamUser -> Tuples.of(exerciseTeamUser.getUser(), team.getName())) + ); + } + throw new UnsupportedOperationException("Unsupported type of Injection"); } - throw new UnsupportedOperationException("Unsupported type of Injection"); - } - private List usersFromInjection(Injection injection) { - return getUsersFromInjection(injection) - .collect(groupingBy(Tuple2::getT1)).entrySet().stream() - .map(entry -> this.executionContextService.executionContext(entry.getKey(), injection, - entry.getValue().stream().flatMap(ua -> Stream.of(ua.getT2())).toList())) - .toList(); - } + private List usersFromInjection(Injection injection) { + return getUsersFromInjection(injection) + .collect(groupingBy(Tuple2::getT1)).entrySet().stream() + .map(entry -> this.executionContextService.executionContext(entry.getKey(), injection, + entry.getValue().stream().flatMap(ua -> Stream.of(ua.getT2())).toList())) + .toList(); + } - private boolean isBeforeOrEqualsNow(Injection injection) { - Instant now = Instant.now(); - Instant injectWhen = injection.getDate().orElseThrow(); - return injectWhen.equals(now) || injectWhen.isBefore(now); - } + private boolean isBeforeOrEqualsNow(Injection injection) { + Instant now = Instant.now(); + Instant injectWhen = injection.getDate().orElseThrow(); + return injectWhen.equals(now) || injectWhen.isBefore(now); + } - @Transactional - public List getInjectsToRun() { - // Get injects - List injects = this.injectRepository.findAll(InjectSpecification.executable()); - Stream executableInjects = injects.stream() - .filter(this::isBeforeOrEqualsNow) - .sorted(Inject.executionComparator) - .map(inject -> { - Contract contract = this.contractService.resolveContract(inject); - List audiences = getInjectAudiences(inject); - return new ExecutableInject(true, false, inject, contract, audiences, usersFromInjection(inject)); - }); - // Get dry injects - List dryInjects = this.dryInjectRepository.findAll(DryInjectSpecification.executable()); - Stream executableDryInjects = dryInjects.stream() - .filter(this::isBeforeOrEqualsNow) - .sorted(DryInject.executionComparator) - .map(dry -> { - Inject inject = dry.getInject(); - Contract contract = this.contractService.resolveContract(inject); - List audiences = new ArrayList<>(); // No audiences in dry run, only direct users - return new ExecutableInject(false, false, dry, inject, contract, audiences, usersFromInjection(dry)); - }); - // Combine injects and dry - return concat(executableInjects, executableDryInjects) - .filter( - executableInject -> executableInject.getContract() == null || !executableInject.getContract().isManual() - ) - .collect(Collectors.toList()); - } + @Transactional + public List getInjectsToRun() { + // Get injects + List injects = this.injectRepository.findAll(InjectSpecification.executable()); + Stream executableInjects = injects.stream() + .filter(this::isBeforeOrEqualsNow) + .sorted(Inject.executionComparator) + .map(inject -> { + Contract contract = this.contractService.resolveContract(inject); + List teams = getInjectTeams(inject); + return new ExecutableInject(true, false, inject, contract, teams, usersFromInjection(inject)); + }); + // Get dry injects + List dryInjects = this.dryInjectRepository.findAll(DryInjectSpecification.executable()); + Stream executableDryInjects = dryInjects.stream() + .filter(this::isBeforeOrEqualsNow) + .sorted(DryInject.executionComparator) + .map(dry -> { + Inject inject = dry.getInject(); + Contract contract = this.contractService.resolveContract(inject); + List teams = new ArrayList<>(); // No teams in dry run, only direct users + return new ExecutableInject(false, false, dry, inject, contract, teams, usersFromInjection(dry)); + }); + // Combine injects and dry + return concat(executableInjects, executableDryInjects) + .filter( + executableInject -> executableInject.getContract() == null || !executableInject.getContract().isManual() + ) + .collect(Collectors.toList()); + } } diff --git a/openex-api/src/main/java/io/openex/importer/V1_DataImporter.java b/openex-api/src/main/java/io/openex/importer/V1_DataImporter.java index e357ca03cc..0f0e229bf1 100644 --- a/openex-api/src/main/java/io/openex/importer/V1_DataImporter.java +++ b/openex-api/src/main/java/io/openex/importer/V1_DataImporter.java @@ -39,7 +39,7 @@ public class V1_DataImporter implements Importer { private DocumentRepository documentRepository; private TagRepository tagRepository; private ExerciseRepository exerciseRepository; - private AudienceRepository audienceRepository; + private TeamRepository teamRepository; private ObjectiveRepository objectiveRepository; private InjectRepository injectRepository; private OrganizationRepository organizationRepository; @@ -120,8 +120,8 @@ public void setExerciseRepository(ExerciseRepository exerciseRepository) { } @Autowired - public void setAudienceRepository(AudienceRepository audienceRepository) { - this.audienceRepository = audienceRepository; + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; } @Autowired @@ -207,8 +207,8 @@ private void importInjects(Map baseIds, Exercise exercise, List baseIds, Exercise exercise, List injectAudienceIds = resolveJsonIds(injectNode, "inject_audiences"); - injectAudienceIds.forEach(audienceId -> { - String remappedId = baseIds.get(audienceId).getId(); - injectRepository.addAudience(injectId, remappedId); + // Teams + List injectTeamIds = resolveJsonIds(injectNode, "inject_teams"); + injectTeamIds.forEach(teamId -> { + String remappedId = baseIds.get(teamId).getId(); + injectRepository.addTeam(injectId, remappedId); }); // Documents List injectDocuments = resolveJsonElements(injectNode, "inject_documents").toList(); @@ -408,25 +408,33 @@ public void importData(JsonNode importNode, Map docReferenc }); } - // ------------ Handling audiences - Iterator exerciseAudiences = importNode.get("exercise_audiences").elements(); - exerciseAudiences.forEachRemaining(nodeAudience -> { - String id = nodeAudience.get("audience_id").textValue(); - Audience audience = new Audience(); - audience.setName(nodeAudience.get("audience_name").textValue()); - audience.setDescription(nodeAudience.get("audience_description").textValue()); - // Tags - List audienceTagIds = resolveJsonIds(nodeAudience, "audience_tags"); - List tagsForAudience = audienceTagIds.stream().map(baseIds::get).map(base -> (Tag) base).toList(); - audience.setTags(tagsForAudience); - // Users - List audienceUserIds = resolveJsonIds(nodeAudience, "audience_users"); - List usersForAudience = audienceUserIds.stream().map(baseIds::get).map(base -> (User) base).toList(); - audience.setUsers(usersForAudience); - // Finalize - audience.setExercise(savedExercise); - Audience savedAudience = audienceRepository.save(audience); - baseIds.put(id, savedAudience); + // ------------ Handling teams + Iterator exerciseTeams = importNode.get("exercise_teams").elements(); + exerciseTeams.forEachRemaining(nodeTeam -> { + String id = nodeTeam.get("team_id").textValue(); + String teamName = nodeTeam.get("team_name").textValue(); + // Prevent duplication of team, based on the team name + List existingTeams = teamRepository.findByNameIgnoreCase(teamName); + if (existingTeams.size() == 1) { + baseIds.put(id, existingTeams.get(0)); + } else { + Team team = new Team(); + team.setName(nodeTeam.get("team_name").textValue()); + team.setDescription(nodeTeam.get("team_description").textValue()); + // Tags + List teamTagIds = resolveJsonIds(nodeTeam, "team_tags"); + List tagsForTeam = teamTagIds.stream().map(baseIds::get).map(base -> (Tag) base).toList(); + team.setTags(tagsForTeam); + // Users + List teamUserIds = resolveJsonIds(nodeTeam, "team_users"); + List usersForTeam = teamUserIds.stream().map(baseIds::get).map(base -> (User) base).toList(); + team.setUsers(usersForTeam); + List savedExercises = new ArrayList<>(); + savedExercises.add(savedExercise); + team.setExercises(savedExercises); + Team savedTeam = teamRepository.save(team); + baseIds.put(id, savedTeam); + } }); // ------------ Handling challenges @@ -550,11 +558,11 @@ public void importData(JsonNode importNode, Map docReferenc lessonsCategory.setDescription(nodeLessonCategory.get("lessons_category_description").textValue()); lessonsCategory.setOrder(nodeLessonCategory.get("lessons_category_order").intValue()); lessonsCategory.setExercise(exercise); - List lessonsCategoryAudiences = resolveJsonIds(nodeLessonCategory, "lessons_category_audiences") - .stream().map(audienceId -> (Audience) baseIds.get(audienceId)) + List lessonsCategoryTeams = resolveJsonIds(nodeLessonCategory, "lessons_category_teams") + .stream().map(teamId -> (Team) baseIds.get(teamId)) .filter(Objects::nonNull) .toList(); - lessonsCategory.setAudiences(lessonsCategoryAudiences); + lessonsCategory.setTeams(lessonsCategoryTeams); LessonsCategory savedLessonsCategory = lessonsCategoryRepository.save(lessonsCategory); baseIds.put(id, savedLessonsCategory); }); diff --git a/openex-api/src/main/java/io/openex/injects/challenge/ChallengeContract.java b/openex-api/src/main/java/io/openex/injects/challenge/ChallengeContract.java index d8c5690f7d..401f252e1c 100644 --- a/openex-api/src/main/java/io/openex/injects/challenge/ChallengeContract.java +++ b/openex-api/src/main/java/io/openex/injects/challenge/ChallengeContract.java @@ -14,7 +14,7 @@ import static io.openex.contract.ContractDef.contractBuilder; import static io.openex.contract.fields.ContractChallenge.challengeField; import static io.openex.contract.fields.ContractAttachment.attachmentField; -import static io.openex.contract.fields.ContractAudience.audienceField; +import static io.openex.contract.fields.ContractTeam.teamField; import static io.openex.contract.fields.ContractCheckbox.checkboxField; import static io.openex.contract.fields.ContractText.textField; import static io.openex.contract.fields.ContractTextArea.richTextareaField; @@ -63,7 +63,7 @@ public List contracts() { .mandatory(textField("subject", "Subject", "New challenges published for ${user.email}")) .mandatory(richTextareaField("body", "Body", messageBody)) .optional(checkboxField("encrypted", "Encrypted", false)) - .mandatory(audienceField("audiences", "Audiences", Multiple)) + .mandatory(teamField("audiences", "Audiences", Multiple)) .optional(attachmentField("attachments", "Attachments", Multiple)) .build(); Contract publishChallenge = executableContract(contractConfig, diff --git a/openex-api/src/main/java/io/openex/injects/email/EmailContract.java b/openex-api/src/main/java/io/openex/injects/email/EmailContract.java index bb04ed2b38..15da61361b 100644 --- a/openex-api/src/main/java/io/openex/injects/email/EmailContract.java +++ b/openex-api/src/main/java/io/openex/injects/email/EmailContract.java @@ -18,7 +18,7 @@ import static io.openex.contract.ContractDef.contractBuilder; import static io.openex.contract.ContractVariable.variable; import static io.openex.contract.fields.ContractAttachment.attachmentField; -import static io.openex.contract.fields.ContractAudience.audienceField; +import static io.openex.contract.fields.ContractTeam.teamField; import static io.openex.contract.fields.ContractCheckbox.checkboxField; import static io.openex.contract.fields.ContractExpectations.expectationsField; import static io.openex.contract.fields.ContractText.textField; @@ -47,7 +47,7 @@ public String getType() { public ContractConfig getConfig() { return new ContractConfig(TYPE, Map.of(en, "Email", fr, "Email"), "#cddc39", "/img/email.png", isExpose()); } - + @Override public List contracts() { // variables @@ -60,7 +60,7 @@ public List contracts() { ContractConfig contractConfig = getConfig(); // Standard contract List standardInstance = contractBuilder() - .mandatory(audienceField("audiences", "Audiences", Multiple)) + .mandatory(teamField("teams", "Teams", Multiple)) .mandatory(textField("subject", "Subject")) .mandatory(richTextareaField("body", "Body")) // .optional(textField("inReplyTo", "InReplyTo", "HIDDEN")) - Use for direct injection @@ -73,7 +73,7 @@ public List contracts() { standardEmail.addVariable(documentUriVariable); // Global contract List globalInstance = contractBuilder() - .mandatory(audienceField("audiences", "Audiences", Multiple)) + .mandatory(teamField("teams", "Teams", Multiple)) .mandatory(textField("subject", "Subject")) .mandatory(richTextareaField("body", "Body")) // .mandatory(textField("inReplyTo", "InReplyTo", "HIDDEN")) - Use for direct injection diff --git a/openex-api/src/main/java/io/openex/injects/media/MediaContract.java b/openex-api/src/main/java/io/openex/injects/media/MediaContract.java index 21e82c9b54..e5d793e0c3 100644 --- a/openex-api/src/main/java/io/openex/injects/media/MediaContract.java +++ b/openex-api/src/main/java/io/openex/injects/media/MediaContract.java @@ -20,7 +20,7 @@ import static io.openex.contract.ContractVariable.variable; import static io.openex.contract.fields.ContractArticle.articleField; import static io.openex.contract.fields.ContractAttachment.attachmentField; -import static io.openex.contract.fields.ContractAudience.audienceField; +import static io.openex.contract.fields.ContractTeam.teamField; import static io.openex.contract.fields.ContractCheckbox.checkboxField; import static io.openex.contract.fields.ContractExpectations.expectationsField; import static io.openex.contract.fields.ContractText.textField; @@ -71,14 +71,14 @@ public List contracts() { ContractCheckbox emailingField = checkboxField("emailing", "Send email", true); Expectation expectation = new Expectation(); expectation.setType(ARTICLE); - expectation.setName("Expect audiences to read the article(s)"); + expectation.setName("Expect teams to read the article(s)"); expectation.setScore(0); ContractExpectations expectationsField = expectationsField( "expectations", "Expectations", List.of(expectation) ); List publishInstance = contractBuilder() // built in - .optional(audienceField("audiences", "Audiences", Multiple)) + .optional(teamField("teams", "Teams", Multiple)) .optional(attachmentField("attachments", "Attachments", Multiple)) .mandatory(articleField("articles", "Articles", Multiple)) // Contract specific diff --git a/openex-api/src/main/java/io/openex/migration/V2_64__Audiences_detach_exercises.java b/openex-api/src/main/java/io/openex/migration/V2_64__Audiences_detach_exercises.java new file mode 100644 index 0000000000..86ff0b1a9a --- /dev/null +++ b/openex-api/src/main/java/io/openex/migration/V2_64__Audiences_detach_exercises.java @@ -0,0 +1,97 @@ +package io.openex.migration; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +import java.sql.Connection; +import java.sql.Statement; + +@Component +public class V2_64__Audiences_detach_exercises extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + Connection connection = context.getConnection(); + Statement select = connection.createStatement(); + // Add Variable table + select.execute(""" + ALTER TABLE audiences RENAME CONSTRAINT subaudiences_pkey TO team_pkey; + ALTER TABLE audiences DROP CONSTRAINT fk_audience_exercise; + ALTER TABLE audiences DROP column audience_enabled; + ALTER TABLE audiences DROP column audience_exercise; + ALTER TABLE audiences RENAME COLUMN audience_id TO team_id; + ALTER TABLE audiences RENAME COLUMN audience_name TO team_name; + ALTER TABLE audiences RENAME COLUMN audience_description TO team_description; + ALTER TABLE audiences RENAME COLUMN audience_created_at TO team_created_at; + ALTER TABLE audiences RENAME COLUMN audience_updated_at TO team_updated_at; + ALTER TABLE audiences RENAME TO teams; + """); + select.execute(""" + ALTER TABLE teams ADD COLUMN team_organization varchar(255); + ALTER TABLE teams ADD CONSTRAINT fk_teams_organizations FOREIGN KEY (team_organization) REFERENCES organizations(organization_id) ON DELETE SET NULL; + """); + select.execute(""" + CREATE TABLE exercises_teams ( + exercise_id varchar(255) not null + constraint exercise_id_fk + references exercises + on delete cascade, + team_id varchar(255) not null + constraint team_id_fk + references teams + on delete cascade, + primary key (exercise_id, team_id) + ); + CREATE INDEX idx_exercises_teams_exercise on exercises_teams (exercise_id); + CREATE INDEX idx_exercises_teams_team on exercises_teams (team_id); + """); + select.execute(""" + CREATE TABLE exercises_teams_users ( + exercise_id varchar(255) not null + constraint exercise_id_fk + references exercises + on delete cascade, + team_id varchar(255) not null + constraint team_id_fk + references teams + on delete cascade, + user_id varchar(255) not null + constraint user_id_fk + references users + on delete cascade, + primary key (exercise_id, team_id, user_id) + ); + CREATE INDEX idx_exercises_teams_users_exercise on exercises_teams_users (exercise_id); + CREATE INDEX idx_exercises_teams_users_team on exercises_teams_users (team_id); + CREATE INDEX idx_exercises_teams_users_user on exercises_teams_users (user_id); + """); + select.execute(""" + ALTER TABLE users_audiences RENAME CONSTRAINT users_subaudiences_pkey TO users_teams_pkey; + ALTER TABLE users_audiences RENAME COLUMN audience_id TO team_id; + ALTER TABLE users_audiences RENAME to users_teams; + """); + select.execute(""" + ALTER TABLE audiences_tags RENAME CONSTRAINT audiences_tags_pkey TO teams_tags_pkey; + ALTER TABLE audiences_tags RENAME CONSTRAINT audience_id_fk TO team_id_fk; + ALTER TABLE audiences_tags RENAME COLUMN audience_id TO team_id; + ALTER TABLE audiences_tags RENAME to teams_tags; + """); + select.execute(""" + ALTER TABLE injects_audiences RENAME CONSTRAINT injects_subaudiences_pkey TO injects_teams_pkey; + ALTER TABLE injects_audiences RENAME COLUMN audience_id TO team_id; + ALTER TABLE injects_audiences RENAME to injects_teams; + """); + select.execute(""" + ALTER TABLE lessons_categories_audiences RENAME CONSTRAINT lessons_categories_audiences_pkey TO lessons_categories_teams_pkey; + ALTER TABLE lessons_categories_audiences RENAME CONSTRAINT audience_id_fk TO team_id_fk; + ALTER TABLE lessons_categories_audiences RENAME COLUMN audience_id TO team_id; + ALTER TABLE lessons_categories_audiences RENAME to lessons_categories_teams; + """); + select.execute(""" + ALTER TABLE injects RENAME COLUMN inject_all_audiences TO inject_all_teams; + ALTER TABLE injects_expectations RENAME COLUMN audience_id TO team_id; + ALTER TABLE injects_expectations RENAME CONSTRAINT fk_expectations_audience TO fk_expectations_team; + """); + } +} diff --git a/openex-api/src/main/java/io/openex/rest/audience/AudienceApi.java b/openex-api/src/main/java/io/openex/rest/audience/AudienceApi.java deleted file mode 100644 index 824646c13d..0000000000 --- a/openex-api/src/main/java/io/openex/rest/audience/AudienceApi.java +++ /dev/null @@ -1,126 +0,0 @@ -package io.openex.rest.audience; - -import io.openex.database.model.Audience; -import io.openex.database.model.Exercise; -import io.openex.database.model.User; -import io.openex.database.repository.AudienceRepository; -import io.openex.database.repository.ExerciseRepository; -import io.openex.database.repository.TagRepository; -import io.openex.database.repository.UserRepository; -import io.openex.database.specification.AudienceSpecification; -import io.openex.rest.audience.form.AudienceCreateInput; -import io.openex.rest.audience.form.AudienceUpdateActivationInput; -import io.openex.rest.audience.form.AudienceUpdateInput; -import io.openex.rest.audience.form.UpdateUsersAudienceInput; -import io.openex.rest.helper.RestBehavior; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.security.RolesAllowed; -import javax.validation.Valid; - -import static io.openex.database.model.User.ROLE_USER; -import static io.openex.helper.StreamHelper.fromIterable; -import static java.time.Instant.now; - -@RestController -@RolesAllowed(ROLE_USER) -public class AudienceApi extends RestBehavior { - - private ExerciseRepository exerciseRepository; - private AudienceRepository audienceRepository; - private UserRepository userRepository; - private TagRepository tagRepository; - - @Autowired - public void setUserRepository(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Autowired - public void setExerciseRepository(ExerciseRepository exerciseRepository) { - this.exerciseRepository = exerciseRepository; - } - - @Autowired - public void setAudienceRepository(AudienceRepository audienceRepository) { - this.audienceRepository = audienceRepository; - } - - @Autowired - public void setTagRepository(TagRepository tagRepository) { - this.tagRepository = tagRepository; - } - - @GetMapping("/api/exercises/{exerciseId}/audiences") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public Iterable getAudiences(@PathVariable String exerciseId) { - return audienceRepository.findAll(AudienceSpecification.fromExercise(exerciseId)); - } - - @GetMapping("/api/exercises/{exerciseId}/audiences/{audienceId}") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public Audience getAudience(@PathVariable String exerciseId, @PathVariable String audienceId) { - return audienceRepository.findById(audienceId).orElseThrow(); - } - - @GetMapping("/api/exercises/{exerciseId}/audiences/{audienceId}/players") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public Iterable getAudiencePlayers(@PathVariable String exerciseId, @PathVariable String audienceId) { - return audienceRepository.findById(audienceId).orElseThrow().getUsers(); - } - - @PostMapping("/api/exercises/{exerciseId}/audiences") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Audience createAudience(@PathVariable String exerciseId, - @Valid @RequestBody AudienceCreateInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - Audience audience = new Audience(); - audience.setUpdateAttributes(input); - audience.setExercise(exercise); - audience.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); - return audienceRepository.save(audience); - } - - @DeleteMapping("/api/exercises/{exerciseId}/audiences/{audienceId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public void deleteAudience(@PathVariable String exerciseId, @PathVariable String audienceId) { - audienceRepository.deleteById(audienceId); - } - - @PutMapping("/api/exercises/{exerciseId}/audiences/{audienceId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Audience updateAudience(@PathVariable String exerciseId, - @PathVariable String audienceId, - @Valid @RequestBody AudienceUpdateInput input) { - Audience audience = audienceRepository.findById(audienceId).orElseThrow(); - audience.setUpdateAttributes(input); - audience.setUpdatedAt(now()); - audience.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); - return audienceRepository.save(audience); - } - - @PutMapping("/api/exercises/{exerciseId}/audiences/{audienceId}/players") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Audience updateAudienceUsers( - @PathVariable String exerciseId, - @PathVariable String audienceId, - @Valid @RequestBody UpdateUsersAudienceInput input) { - Audience audience = audienceRepository.findById(audienceId).orElseThrow(); - Iterable audienceUsers = userRepository.findAllById(input.getUserIds()); - audience.setUsers(fromIterable(audienceUsers)); - return audienceRepository.save(audience); - } - - @PutMapping("/api/exercises/{exerciseId}/audiences/{audienceId}/activation") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Audience updateAudienceActivation( - @PathVariable String exerciseId, - @PathVariable String audienceId, - @Valid @RequestBody AudienceUpdateActivationInput input) { - Audience audience = audienceRepository.findById(audienceId).orElseThrow(); - audience.setEnabled(input.isEnabled()); - return audienceRepository.save(audience); - } -} diff --git a/openex-api/src/main/java/io/openex/rest/challenge/ChallengeApi.java b/openex-api/src/main/java/io/openex/rest/challenge/ChallengeApi.java index 7a00912082..661cbfbbb0 100644 --- a/openex-api/src/main/java/io/openex/rest/challenge/ChallengeApi.java +++ b/openex-api/src/main/java/io/openex/rest/challenge/ChallengeApi.java @@ -145,9 +145,9 @@ public ChallengesReader playerChallenges(@PathVariable String exerciseId, @Reque throw new UnsupportedOperationException("User must be logged or dynamic player is required"); } ChallengesReader reader = new ChallengesReader(exercise); - List audienceIds = user.getAudiences().stream().map(Audience::getId).toList(); + List teamIds = user.getTeams().stream().map(Team::getId).toList(); List challengeExpectations = injectExpectationRepository.findChallengeExpectations(exerciseId, - audienceIds); + teamIds); List challenges = challengeExpectations.stream() .map(injectExpectation -> { Challenge challenge = injectExpectation.getChallenge(); @@ -215,9 +215,9 @@ public ChallengesReader validateChallenge(@PathVariable String exerciseId, } ChallengeResult challengeResult = tryChallenge(challengeId, input); if (challengeResult.getResult()) { - List audienceIds = user.getAudiences().stream().map(Audience::getId).toList(); + List teamIds = user.getTeams().stream().map(Team::getId).toList(); List challengeExpectations = injectExpectationRepository.findChallengeExpectations(exerciseId, - audienceIds, challengeId); + teamIds, challengeId); challengeExpectations.forEach(injectExpectationExecution -> { injectExpectationExecution.setUser(user); injectExpectationExecution.setResult(Instant.now().toString()); diff --git a/openex-api/src/main/java/io/openex/rest/comcheck/ComcheckApi.java b/openex-api/src/main/java/io/openex/rest/comcheck/ComcheckApi.java index b670539477..9756784a48 100644 --- a/openex-api/src/main/java/io/openex/rest/comcheck/ComcheckApi.java +++ b/openex-api/src/main/java/io/openex/rest/comcheck/ComcheckApi.java @@ -1,7 +1,7 @@ package io.openex.rest.comcheck; import io.openex.database.model.*; -import io.openex.database.repository.AudienceRepository; +import io.openex.database.repository.TeamRepository; import io.openex.database.repository.ComcheckRepository; import io.openex.database.repository.ComcheckStatusRepository; import io.openex.database.repository.ExerciseRepository; @@ -22,7 +22,7 @@ public class ComcheckApi extends RestBehavior { private ComcheckRepository comcheckRepository; - private AudienceRepository audienceRepository; + private TeamRepository teamRepository; private ExerciseRepository exerciseRepository; private ComcheckStatusRepository comcheckStatusRepository; @@ -37,8 +37,8 @@ public void setComcheckRepository(ComcheckRepository comcheckRepository) { } @Autowired - public void setAudienceRepository(AudienceRepository audienceRepository) { - this.audienceRepository = audienceRepository; + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; } @Autowired @@ -83,10 +83,10 @@ public Comcheck communicationCheck(@PathVariable String exerciseId, Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); check.setExercise(exercise); // 02. Get users - List audienceIds = comCheck.getAudienceIds(); - List audiences = audienceIds.isEmpty() ? exercise.getAudiences() : - fromIterable(audienceRepository.findAllById(audienceIds)); - List users = audiences.stream().flatMap(audience -> audience.getUsers().stream()).distinct().toList(); + List teamIds = comCheck.getTeamIds(); + List teams = teamIds.isEmpty() ? exercise.getTeams() : + fromIterable(teamRepository.findAllById(teamIds)); + List users = teams.stream().flatMap(team -> team.getUsers().stream()).distinct().toList(); List comcheckStatuses = users.stream().map(user -> { ComcheckStatus comcheckStatus = new ComcheckStatus(user); comcheckStatus.setComcheck(check); diff --git a/openex-api/src/main/java/io/openex/rest/comcheck/form/ComcheckInput.java b/openex-api/src/main/java/io/openex/rest/comcheck/form/ComcheckInput.java index 4adfeef96b..7e50653b99 100644 --- a/openex-api/src/main/java/io/openex/rest/comcheck/form/ComcheckInput.java +++ b/openex-api/src/main/java/io/openex/rest/comcheck/form/ComcheckInput.java @@ -23,8 +23,8 @@ public class ComcheckInput { @JsonProperty("comcheck_message") private String message; - @JsonProperty("comcheck_audiences") - private List audienceIds = new ArrayList<>(); + @JsonProperty("comcheck_teams") + private List teamIds = new ArrayList<>(); public String getName() { return name; @@ -34,12 +34,12 @@ public void setName(String name) { this.name = name; } - public List getAudienceIds() { - return audienceIds; + public List getTeamIds() { + return teamIds; } - public void setAudienceIds(List audienceIds) { - this.audienceIds = audienceIds; + public void setTeamIds(List teamIds) { + this.teamIds = teamIds; } public Instant getEnd() { diff --git a/openex-api/src/main/java/io/openex/rest/document/DocumentApi.java b/openex-api/src/main/java/io/openex/rest/document/DocumentApi.java index 0291d2c8b3..0acf3cdef4 100644 --- a/openex-api/src/main/java/io/openex/rest/document/DocumentApi.java +++ b/openex-api/src/main/java/io/openex/rest/document/DocumentApi.java @@ -231,7 +231,7 @@ public List playerDocuments(@PathVariable String exerciseId, @RequestP if (user.getId().equals(ANONYMOUS)) { throw new UnsupportedOperationException("User must be logged or dynamic player is required"); } - if (!exercise.isUserHasAccess(user) && !exercise.getPlayers().contains(user)) { + if (!exercise.isUserHasAccess(user) && !exercise.getUsers().contains(user)) { throw new UnsupportedOperationException("The given player is not in this exercise"); } return getExercisePlayerDocuments(exercise); @@ -245,7 +245,7 @@ public void downloadPlayerDocument(@PathVariable String exerciseId, @PathVariabl if (user.getId().equals(ANONYMOUS)) { throw new UnsupportedOperationException("User must be logged or dynamic player is required"); } - if (!exercise.isUserHasAccess(user) && !exercise.getPlayers().contains(user)) { + if (!exercise.isUserHasAccess(user) && !exercise.getUsers().contains(user)) { throw new UnsupportedOperationException("The given player is not in this exercise"); } Document document = getExercisePlayerDocuments(exercise).stream().filter(doc -> doc.getId().equals(documentId)) diff --git a/openex-api/src/main/java/io/openex/rest/exercise/ExerciseApi.java b/openex-api/src/main/java/io/openex/rest/exercise/ExerciseApi.java index b0920c3285..c6cd09bb75 100644 --- a/openex-api/src/main/java/io/openex/rest/exercise/ExerciseApi.java +++ b/openex-api/src/main/java/io/openex/rest/exercise/ExerciseApi.java @@ -55,593 +55,642 @@ @RolesAllowed(ROLE_USER) public class ExerciseApi extends RestBehavior { - private static final Logger LOGGER = Logger.getLogger(ExerciseApi.class.getName()); - - // region properties - @Value("${openex.mail.imap.enabled}") - private boolean imapEnabled; - - @Value("${openex.mail.imap.username}") - private String imapUsername; - - @Resource - private OpenExConfig openExConfig; - // endregion - - // region repositories - private LogRepository logRepository; - private TagRepository tagRepository; - private UserRepository userRepository; - private PauseRepository pauseRepository; - private GroupRepository groupRepository; - private GrantRepository grantRepository; - private DocumentRepository documentRepository; - private ExerciseRepository exerciseRepository; - private LogRepository exerciseLogRepository; - private DryRunRepository dryRunRepository; - private DryInjectRepository dryInjectRepository; - private ComcheckRepository comcheckRepository; - private ImportService importService; - private InjectRepository injectRepository; - private LessonsCategoryRepository lessonsCategoryRepository; - private LessonsQuestionRepository lessonsQuestionRepository; - private LessonsAnswerRepository lessonsAnswerRepository; - // endregion - - // region services - private DryrunService dryrunService; - private FileService fileService; - private InjectService injectService; - private ChallengeService challengeService; - private VariableService variableService; - // endregion - - // region setters - @Autowired - public void setChallengeService(ChallengeService challengeService) { - this.challengeService = challengeService; - } - - @Autowired - public void setInjectService(InjectService injectService) { - this.injectService = injectService; - } - - @Autowired - public void setImportService(ImportService importService) { - this.importService = importService; - } - - @Autowired - public void setLogRepository(LogRepository logRepository) { - this.logRepository = logRepository; - } - - @Autowired - public void setUserRepository(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Autowired - public void setPauseRepository(PauseRepository pauseRepository) { - this.pauseRepository = pauseRepository; - } - - @Autowired - public void setGrantRepository(GrantRepository grantRepository) { - this.grantRepository = grantRepository; - } - - @Autowired - public void setGroupRepository(GroupRepository groupRepository) { - this.groupRepository = groupRepository; - } - - @Autowired - public void setDryrunService(DryrunService dryrunService) { - this.dryrunService = dryrunService; - } - - @Autowired - public void setInjectRepository(InjectRepository injectRepository) { - this.injectRepository = injectRepository; - } - - @Autowired - public void setFileService(FileService fileService) { - this.fileService = fileService; - } - - @Autowired - public void setTagRepository(TagRepository tagRepository) { - this.tagRepository = tagRepository; - } - - @Autowired - public void setDocumentRepository(DocumentRepository documentRepository) { - this.documentRepository = documentRepository; - } - - @Autowired - public void setComcheckRepository(ComcheckRepository comcheckRepository) { - this.comcheckRepository = comcheckRepository; - } - - @Autowired - public void setDryRunRepository(DryRunRepository dryRunRepository) { - this.dryRunRepository = dryRunRepository; - } - - @Autowired - public void setDryInjectRepository(DryInjectRepository dryInjectRepository) { - this.dryInjectRepository = dryInjectRepository; - } - - @Autowired - public void setExerciseLogRepository(LogRepository exerciseLogRepository) { - this.exerciseLogRepository = exerciseLogRepository; - } - - @Autowired - public void setExerciseRepository(ExerciseRepository exerciseRepository) { - this.exerciseRepository = exerciseRepository; - } - - @Autowired - public void setLessonsCategoryRepository(LessonsCategoryRepository lessonsCategoryRepository) { - this.lessonsCategoryRepository = lessonsCategoryRepository; - } - - @Autowired - public void setLessonsQuestionRepository(LessonsQuestionRepository lessonsQuestionRepository) { - this.lessonsQuestionRepository = lessonsQuestionRepository; - } - - @Autowired - public void setLessonsAnswerRepository(LessonsAnswerRepository lessonsAnswerRepository) { - this.lessonsAnswerRepository = lessonsAnswerRepository; - } - - @Autowired - public void setVariableService(@NotNull final VariableService variableService) { - this.variableService = variableService; - } - // endregion - - // region logs - @GetMapping("/api/exercises/{exercise}/logs") - public Iterable logs(@PathVariable String exercise) { - return exerciseLogRepository.findAll(ExerciseLogSpecification.fromExercise(exercise)); - } - - @PostMapping("/api/exercises/{exerciseId}/logs") - public Log createLog(@PathVariable String exerciseId, @Valid @RequestBody LogCreateInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - Log log = new Log(); - log.setUpdateAttributes(input); - log.setExercise(exercise); - log.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); - log.setUser(userRepository.findById(currentUser().getId()).orElseThrow()); - return exerciseLogRepository.save(log); - } - - @PutMapping("/api/exercises/{exerciseId}/logs/{logId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Log updateLog(@PathVariable String exerciseId, @PathVariable String logId, - @Valid @RequestBody LogCreateInput input) { - Log log = logRepository.findById(logId).orElseThrow(); - log.setUpdateAttributes(input); - log.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); - return logRepository.save(log); - } - - @DeleteMapping("/api/exercises/{exerciseId}/logs/{logId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public void deleteLog(@PathVariable String exerciseId, @PathVariable String logId) { - logRepository.deleteById(logId); - } - // endregion - - // region dryruns - @GetMapping("/api/exercises/{exerciseId}/dryruns") - public Iterable dryruns(@PathVariable String exerciseId) { - return dryRunRepository.findAll(DryRunSpecification.fromExercise(exerciseId)); - } - - @PostMapping("/api/exercises/{exerciseId}/dryruns") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Dryrun createDryrun(@PathVariable String exerciseId, @Valid @RequestBody DryrunCreateInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - List userIds = input.getUserIds(); - List users = userIds.size() == 0 ? List.of(userRepository.findById(currentUser().getId()).orElseThrow()) - : fromIterable(userRepository.findAllById(userIds)); - return dryrunService.provisionDryrun(exercise, users, input.getName()); - } - - @GetMapping("/api/exercises/{exerciseId}/dryruns/{dryrunId}") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public Dryrun dryrun(@PathVariable String exerciseId, @PathVariable String dryrunId) { - Specification filters = DryRunSpecification.fromExercise(exerciseId).and(DryRunSpecification.id(dryrunId)); - return dryRunRepository.findOne(filters).orElseThrow(); - } - - @DeleteMapping("/api/exercises/{exerciseId}/dryruns/{dryrunId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public void deleteDryrun(@PathVariable String exerciseId, @PathVariable String dryrunId) { - dryRunRepository.deleteById(dryrunId); - } - - @GetMapping("/api/exercises/{exerciseId}/dryruns/{dryrunId}/dryinjects") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public List dryrunInjects(@PathVariable String exerciseId, @PathVariable String dryrunId) { - return dryInjectRepository.findAll(DryInjectSpecification.fromDryRun(dryrunId)); - } - // endregion - - // region comchecks - @GetMapping("/api/exercises/{exercise}/comchecks") - public Iterable comchecks(@PathVariable String exercise) { - return comcheckRepository.findAll(ComcheckSpecification.fromExercise(exercise)); - } - - @GetMapping("/api/exercises/{exercise}/comchecks/{comcheck}") - public Comcheck comcheck(@PathVariable String exercise, @PathVariable String comcheck) { - Specification filters = ComcheckSpecification.fromExercise(exercise) - .and(ComcheckSpecification.id(comcheck)); - return comcheckRepository.findOne(filters).orElseThrow(); - } - - @GetMapping("/api/exercises/{exercise}/comchecks/{comcheck}/statuses") - public List comcheckStatuses(@PathVariable String exercise, @PathVariable String comcheck) { - return comcheck(exercise, comcheck).getComcheckStatus(); - } - // endregion - - // region exercises - @Transactional(rollbackOn = Exception.class) - @PostMapping("/api/exercises") - public Exercise createExercise(@Valid @RequestBody ExerciseCreateInput input) { - Exercise exercise = new Exercise(); - exercise.setUpdateAttributes(input); - exercise.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); - if (imapEnabled) { - exercise.setReplyTo(imapUsername); - } else { - exercise.setReplyTo(openExConfig.getDefaultMailer()); - } - // Find automatic groups to grants - List groups = fromIterable(groupRepository.findAll()); - List grants = groups.stream().filter(group -> group.getExercisesDefaultGrants().size() > 0) - .flatMap(group -> group.getExercisesDefaultGrants().stream().map(s -> Tuples.of(group, s))).map(tuple -> { - Grant grant = new Grant(); - grant.setGroup(tuple.getT1()); - grant.setName(tuple.getT2()); - grant.setExercise(exercise); - return grant; + private static final Logger LOGGER = Logger.getLogger(ExerciseApi.class.getName()); + + // region properties + @Value("${openex.mail.imap.enabled}") + private boolean imapEnabled; + + @Value("${openex.mail.imap.username}") + private String imapUsername; + + @Resource + private OpenExConfig openExConfig; + // endregion + + // region repositories + private LogRepository logRepository; + private TagRepository tagRepository; + private UserRepository userRepository; + private PauseRepository pauseRepository; + private GroupRepository groupRepository; + private GrantRepository grantRepository; + private DocumentRepository documentRepository; + private ExerciseRepository exerciseRepository; + private TeamRepository teamRepository; + private ExerciseTeamUserRepository exerciseTeamUserRepository; + private LogRepository exerciseLogRepository; + private DryRunRepository dryRunRepository; + private DryInjectRepository dryInjectRepository; + private ComcheckRepository comcheckRepository; + private ImportService importService; + private InjectRepository injectRepository; + private LessonsCategoryRepository lessonsCategoryRepository; + private LessonsQuestionRepository lessonsQuestionRepository; + private LessonsAnswerRepository lessonsAnswerRepository; + // endregion + + // region services + private DryrunService dryrunService; + private FileService fileService; + private InjectService injectService; + private ChallengeService challengeService; + private VariableService variableService; + // endregion + + // region setters + @Autowired + public void setChallengeService(ChallengeService challengeService) { + this.challengeService = challengeService; + } + + @Autowired + public void setInjectService(InjectService injectService) { + this.injectService = injectService; + } + + @Autowired + public void setImportService(ImportService importService) { + this.importService = importService; + } + + @Autowired + public void setLogRepository(LogRepository logRepository) { + this.logRepository = logRepository; + } + + @Autowired + public void setUserRepository(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Autowired + public void setPauseRepository(PauseRepository pauseRepository) { + this.pauseRepository = pauseRepository; + } + + @Autowired + public void setGrantRepository(GrantRepository grantRepository) { + this.grantRepository = grantRepository; + } + + @Autowired + public void setGroupRepository(GroupRepository groupRepository) { + this.groupRepository = groupRepository; + } + + @Autowired + public void setDryrunService(DryrunService dryrunService) { + this.dryrunService = dryrunService; + } + + @Autowired + public void setInjectRepository(InjectRepository injectRepository) { + this.injectRepository = injectRepository; + } + + @Autowired + public void setFileService(FileService fileService) { + this.fileService = fileService; + } + + @Autowired + public void setTagRepository(TagRepository tagRepository) { + this.tagRepository = tagRepository; + } + + @Autowired + public void setDocumentRepository(DocumentRepository documentRepository) { + this.documentRepository = documentRepository; + } + + @Autowired + public void setComcheckRepository(ComcheckRepository comcheckRepository) { + this.comcheckRepository = comcheckRepository; + } + + @Autowired + public void setDryRunRepository(DryRunRepository dryRunRepository) { + this.dryRunRepository = dryRunRepository; + } + + @Autowired + public void setDryInjectRepository(DryInjectRepository dryInjectRepository) { + this.dryInjectRepository = dryInjectRepository; + } + + @Autowired + public void setExerciseLogRepository(LogRepository exerciseLogRepository) { + this.exerciseLogRepository = exerciseLogRepository; + } + + @Autowired + public void setExerciseRepository(ExerciseRepository exerciseRepository) { + this.exerciseRepository = exerciseRepository; + } + + @Autowired + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; + } + + @Autowired + public void setExerciseTeamUserRepository(ExerciseTeamUserRepository exerciseTeamUserRepository) { + this.exerciseTeamUserRepository = exerciseTeamUserRepository; + } + + @Autowired + public void setLessonsCategoryRepository(LessonsCategoryRepository lessonsCategoryRepository) { + this.lessonsCategoryRepository = lessonsCategoryRepository; + } + + @Autowired + public void setLessonsQuestionRepository(LessonsQuestionRepository lessonsQuestionRepository) { + this.lessonsQuestionRepository = lessonsQuestionRepository; + } + + @Autowired + public void setLessonsAnswerRepository(LessonsAnswerRepository lessonsAnswerRepository) { + this.lessonsAnswerRepository = lessonsAnswerRepository; + } + + @Autowired + public void setVariableService(@NotNull final VariableService variableService) { + this.variableService = variableService; + } + // endregion + + // region logs + @GetMapping("/api/exercises/{exercise}/logs") + public Iterable logs(@PathVariable String exercise) { + return exerciseLogRepository.findAll(ExerciseLogSpecification.fromExercise(exercise)); + } + + @PostMapping("/api/exercises/{exerciseId}/logs") + public Log createLog(@PathVariable String exerciseId, @Valid @RequestBody LogCreateInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + Log log = new Log(); + log.setUpdateAttributes(input); + log.setExercise(exercise); + log.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); + log.setUser(userRepository.findById(currentUser().getId()).orElseThrow()); + return exerciseLogRepository.save(log); + } + + @PutMapping("/api/exercises/{exerciseId}/logs/{logId}") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Log updateLog(@PathVariable String exerciseId, @PathVariable String logId, @Valid @RequestBody LogCreateInput input) { + Log log = logRepository.findById(logId).orElseThrow(); + log.setUpdateAttributes(input); + log.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); + return logRepository.save(log); + } + + @DeleteMapping("/api/exercises/{exerciseId}/logs/{logId}") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public void deleteLog(@PathVariable String exerciseId, @PathVariable String logId) { + logRepository.deleteById(logId); + } + // endregion + + // region dryruns + @GetMapping("/api/exercises/{exerciseId}/dryruns") + public Iterable dryruns(@PathVariable String exerciseId) { + return dryRunRepository.findAll(DryRunSpecification.fromExercise(exerciseId)); + } + + @PostMapping("/api/exercises/{exerciseId}/dryruns") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Dryrun createDryrun(@PathVariable String exerciseId, @Valid @RequestBody DryrunCreateInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + List userIds = input.getUserIds(); + List users = userIds.size() == 0 ? List.of(userRepository.findById(currentUser().getId()).orElseThrow()) : fromIterable(userRepository.findAllById(userIds)); + return dryrunService.provisionDryrun(exercise, users, input.getName()); + } + + @GetMapping("/api/exercises/{exerciseId}/dryruns/{dryrunId}") + @PreAuthorize("isExerciseObserver(#exerciseId)") + public Dryrun dryrun(@PathVariable String exerciseId, @PathVariable String dryrunId) { + Specification filters = DryRunSpecification.fromExercise(exerciseId).and(DryRunSpecification.id(dryrunId)); + return dryRunRepository.findOne(filters).orElseThrow(); + } + + @DeleteMapping("/api/exercises/{exerciseId}/dryruns/{dryrunId}") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public void deleteDryrun(@PathVariable String exerciseId, @PathVariable String dryrunId) { + dryRunRepository.deleteById(dryrunId); + } + + @GetMapping("/api/exercises/{exerciseId}/dryruns/{dryrunId}/dryinjects") + @PreAuthorize("isExerciseObserver(#exerciseId)") + public List dryrunInjects(@PathVariable String exerciseId, @PathVariable String dryrunId) { + return dryInjectRepository.findAll(DryInjectSpecification.fromDryRun(dryrunId)); + } + // endregion + + // region comchecks + @GetMapping("/api/exercises/{exercise}/comchecks") + public Iterable comchecks(@PathVariable String exercise) { + return comcheckRepository.findAll(ComcheckSpecification.fromExercise(exercise)); + } + + @GetMapping("/api/exercises/{exercise}/comchecks/{comcheck}") + public Comcheck comcheck(@PathVariable String exercise, @PathVariable String comcheck) { + Specification filters = ComcheckSpecification.fromExercise(exercise).and(ComcheckSpecification.id(comcheck)); + return comcheckRepository.findOne(filters).orElseThrow(); + } + + @GetMapping("/api/exercises/{exercise}/comchecks/{comcheck}/statuses") + public List comcheckStatuses(@PathVariable String exercise, @PathVariable String comcheck) { + return comcheck(exercise, comcheck).getComcheckStatus(); + } + // endregion + + // region teams + @GetMapping("/api/exercises/{exerciseId}/teams") + @PreAuthorize("isExerciseObserver(#exerciseId)") + public Iterable getExerciseTeams(@PathVariable String exerciseId) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + return exercise.getTeams(); + } + + @Transactional(rollbackOn = Exception.class) + @PutMapping("/api/exercises/{exerciseId}/teams/add") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Iterable addExerciseTeams(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateTeamsInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + List teams = exercise.getTeams(); + teams.addAll(fromIterable(teamRepository.findAllById(input.getTeamIds()))); + exercise.setTeams(teams); + exerciseRepository.save(exercise); + return teamRepository.findAllById(input.getTeamIds()); + } + + @Transactional(rollbackOn = Exception.class) + @PutMapping("/api/exercises/{exerciseId}/teams/remove") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Iterable removeExerciseTeams(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateTeamsInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + // Remove teams from exercise + List teams = exercise.getTeams().stream().filter(team -> !input.getTeamIds().contains(team.getId())).toList(); + exercise.setTeams(fromIterable(teams)); + exerciseRepository.save(exercise); + // Remove all association between users / exercises / teams + input.getTeamIds().forEach(teamId -> { + exerciseTeamUserRepository.deleteTeamFromAllReferences(teamId); + }); + return teamRepository.findAllById(input.getTeamIds()); + } + + @Transactional(rollbackOn = Exception.class) + @PutMapping("/api/exercises/{exerciseId}/teams/{teamId}/players/add") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise addExerciseTeamPlayers(@PathVariable String exerciseId, @PathVariable String teamId, @Valid @RequestBody ExerciseTeamPlayersEnableInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + Team team = teamRepository.findById(teamId).orElseThrow(); + input.getPlayersIds().forEach(playerId -> { + ExerciseTeamUser exerciseTeamUser = new ExerciseTeamUser(); + exerciseTeamUser.setExercise(exercise); + exerciseTeamUser.setTeam(team); + exerciseTeamUser.setUser(userRepository.findById(playerId).orElseThrow()); + exerciseTeamUserRepository.save(exerciseTeamUser); + }); + return exerciseRepository.findById(exerciseId).orElseThrow(); + } + + @Transactional(rollbackOn = Exception.class) + @PutMapping("/api/exercises/{exerciseId}/teams/{teamId}/players/remove") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise removeExerciseTeamPlayers(@PathVariable String exerciseId, @PathVariable String teamId, @Valid @RequestBody ExerciseTeamPlayersEnableInput input) { + input.getPlayersIds().forEach(playerId -> { + ExerciseTeamUserId exerciseTeamUserId = new ExerciseTeamUserId(); + exerciseTeamUserId.setExerciseId(exerciseId); + exerciseTeamUserId.setTeamId(teamId); + exerciseTeamUserId.setUserId(playerId); + exerciseTeamUserRepository.deleteById(exerciseTeamUserId); + }); + return exerciseRepository.findById(exerciseId).orElseThrow(); + } + // endregion + + // region exercises + @Transactional(rollbackOn = Exception.class) + @PostMapping("/api/exercises") + public Exercise createExercise(@Valid @RequestBody ExerciseCreateInput input) { + Exercise exercise = new Exercise(); + exercise.setUpdateAttributes(input); + exercise.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); + if (imapEnabled) { + exercise.setReplyTo(imapUsername); + } else { + exercise.setReplyTo(openExConfig.getDefaultMailer()); + } + // Find automatic groups to grants + List groups = fromIterable(groupRepository.findAll()); + List grants = groups.stream().filter(group -> group.getExercisesDefaultGrants().size() > 0).flatMap(group -> group.getExercisesDefaultGrants().stream().map(s -> Tuples.of(group, s))).map(tuple -> { + Grant grant = new Grant(); + grant.setGroup(tuple.getT1()); + grant.setName(tuple.getT2()); + grant.setExercise(exercise); + return grant; }).toList(); - if (grants.size() > 0) { - Iterable exerciseGrants = grantRepository.saveAll(grants); - exercise.setGrants(fromIterable(exerciseGrants)); - } - return exerciseRepository.save(exercise); - } - - @PutMapping("/api/exercises/{exerciseId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise updateExerciseInformation(@PathVariable String exerciseId, - @Valid @RequestBody ExerciseUpdateInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - exercise.setUpdateAttributes(input); - return exerciseRepository.save(exercise); - } - - @PutMapping("/api/exercises/{exerciseId}/start_date") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise updateExerciseStart(@PathVariable String exerciseId, - @Valid @RequestBody ExerciseUpdateStartDateInput input) throws InputValidationException { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - if (!exercise.getStatus().equals(SCHEDULED)) { - String message = "Change date is only possible in scheduling state"; - throw new InputValidationException("exercise_start_date", message); - } - exercise.setUpdateAttributes(input); - return exerciseRepository.save(exercise); - } - - @PutMapping("/api/exercises/{exerciseId}/tags") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise updateExerciseTags(@PathVariable String exerciseId, - @Valid @RequestBody ExerciseUpdateTagsInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - exercise.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); - return exerciseRepository.save(exercise); - } - - @PutMapping("/api/exercises/{exerciseId}/logos") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise updateExerciseLogos(@PathVariable String exerciseId, - @Valid @RequestBody ExerciseUpdateLogoInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - exercise.setLogoDark(documentRepository.findById(input.getLogoDark()).orElse(null)); - exercise.setLogoLight(documentRepository.findById(input.getLogoLight()).orElse(null)); - return exerciseRepository.save(exercise); - } - - @PutMapping("/api/exercises/{exerciseId}/lessons") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise updateExerciseLessons(@PathVariable String exerciseId, - @Valid @RequestBody ExerciseLessonsInput input) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - exercise.setLessonsAnonymized(input.getLessonsAnonymized()); - return exerciseRepository.save(exercise); - } - - @DeleteMapping("/api/exercises/{exerciseId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public void deleteExercise(@PathVariable String exerciseId) { - exerciseRepository.deleteById(exerciseId); - } - - @GetMapping("/api/exercises/{exerciseId}") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public Exercise exercise(@PathVariable String exerciseId) { - return exerciseRepository.findById(exerciseId).orElseThrow(); - } - - @Transactional(rollbackOn = Exception.class) - @DeleteMapping("/api/exercises/{exerciseId}/{documentId}") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise deleteDocument(@PathVariable String exerciseId, @PathVariable String documentId) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - exercise.setUpdatedAt(now()); - Document doc = documentRepository.findById(documentId).orElseThrow(); - List docExercises = doc.getExercises().stream().filter(ex -> !ex.getId().equals(exerciseId)).toList(); - if (docExercises.isEmpty()) { - // Document is no longer associate to any exercise, delete it - documentRepository.delete(doc); - // All associations with this document will be automatically cleanup. - } else { - // Document associated to other exercise, cleanup - doc.setExercises(docExercises); - documentRepository.save(doc); - // Delete document from all exercise injects - injectService.cleanInjectsDocExercise(exerciseId, documentId); - } - return exerciseRepository.save(exercise); - } - - @Transactional(rollbackOn = Exception.class) - @PutMapping("/api/exercises/{exerciseId}/status") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Exercise changeExerciseStatus(@PathVariable String exerciseId, - @Valid @RequestBody ExerciseUpdateStatusInput input) { - STATUS status = input.getStatus(); - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - // Check if next status is possible - List nextPossibleStatus = exercise.nextPossibleStatus(); - if (!nextPossibleStatus.contains(status)) { - throw new UnsupportedOperationException("Exercise cant support moving to status " + status.name()); - } - // In case of rescheduled of an exercise. - boolean isCloseState = CANCELED.equals(exercise.getStatus()) || FINISHED.equals(exercise.getStatus()); - if (isCloseState && SCHEDULED.equals(status)) { - exercise.setStart(null); - exercise.setEnd(null); - // Reset pauses - exercise.setCurrentPause(null); - pauseRepository.deleteAll(pauseRepository.findAllForExercise(exerciseId)); - // Reset injects outcome, communications and expectations - injectRepository.saveAll(injectRepository.findAllForExercise(exerciseId) - .stream().peek(Inject::clean).toList()); - // Reset lessons learned answers - List lessonsAnswers = lessonsCategoryRepository.findAll( - LessonsCategorySpecification.fromExercise(exerciseId)).stream() - .flatMap(lessonsCategory -> lessonsQuestionRepository.findAll( - LessonsQuestionSpecification.fromCategory(lessonsCategory.getId())).stream() - .flatMap(lessonsQuestion -> lessonsAnswerRepository.findAll( - LessonsAnswerSpecification.fromQuestion(lessonsQuestion.getId())).stream())) - .toList(); - lessonsAnswerRepository.deleteAll(lessonsAnswers); - // Delete exercise transient files (communications, ...) - fileService.deleteDirectory(exerciseId); - } - // In case of manual start - if (SCHEDULED.equals(exercise.getStatus()) && RUNNING.equals(status)) { - Instant nextMinute = now().truncatedTo(MINUTES).plus(1, MINUTES); - exercise.setStart(nextMinute); - } - // If exercise move from pause to running state, - // we log the pause date to be able to recompute inject dates. - if (PAUSED.equals(exercise.getStatus()) && RUNNING.equals(status)) { - Instant lastPause = exercise.getCurrentPause().orElseThrow(); - exercise.setCurrentPause(null); - Pause pause = new Pause(); - pause.setDate(lastPause); - pause.setExercise(exercise); - pause.setDuration(between(lastPause, now()).getSeconds()); - pauseRepository.save(pause); - } - // If pause is asked, just set the pause date. - if (RUNNING.equals(exercise.getStatus()) && PAUSED.equals(status)) { - exercise.setCurrentPause(Instant.now()); - } - // Cancelation - if (RUNNING.equals(exercise.getStatus()) && CANCELED.equals(status)) { - exercise.setEnd(now()); - } - exercise.setUpdatedAt(now()); - exercise.setStatus(status); - return exerciseRepository.save(exercise); - } - - @GetMapping("/api/exercises") - @RolesAllowed(ROLE_USER) - public List exercises() { - Iterable exercises = currentUser().isAdmin() ? exerciseRepository.findAll() - : exerciseRepository.findAllGranted(currentUser().getId()); - return fromIterable(exercises).stream().map(ExerciseSimple::fromExercise).toList(); - } - // endregion - - // region communication - @GetMapping("/api/exercises/{exerciseId}/communications") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public Iterable exerciseCommunications(@PathVariable String exerciseId) { - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - List communications = new ArrayList<>(); - exercise.getInjects().forEach(injectDoc -> communications.addAll(injectDoc.getCommunications())); - return communications; - } - - @GetMapping("/api/communications/attachment") - // @PreAuthorize("isExerciseObserver(#exerciseId)") - public void downloadAttachment(@RequestParam String file, HttpServletResponse response) throws IOException { - FileContainer fileContainer = fileService.getFileContainer(file).orElseThrow(); - response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileContainer.getName()); - response.addHeader(HttpHeaders.CONTENT_TYPE, fileContainer.getContentType()); - response.setStatus(HttpServletResponse.SC_OK); - fileContainer.getInputStream().transferTo(response.getOutputStream()); - } - // endregion - - // region import/export - @GetMapping("/api/exercises/{exerciseId}/export") - @PreAuthorize("isExerciseObserver(#exerciseId)") - public void exerciseExport( - @NotBlank @PathVariable final String exerciseId, - @RequestParam(required = false) final boolean isWithPlayers, - @RequestParam(required = false) final boolean isWithVariableValues, - HttpServletResponse response) throws IOException { - // Setup the mapper for export - List documentIds = new ArrayList<>(); - ObjectMapper objectMapper = mapper.copy(); - if (!isWithPlayers) { - objectMapper.addMixIn(ExerciseFileExport.class, ExerciseExportMixins.ExerciseFileExport.class); - } - // Start exporting exercise - ExerciseFileExport importExport = new ExerciseFileExport(); - importExport.setVersion(1); - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - objectMapper.addMixIn(Exercise.class, ExerciseExportMixins.Exercise.class); - // Build the export - importExport.setExercise(exercise); - importExport.setDocuments(exercise.getDocuments()); - documentIds.addAll(exercise.getDocuments().stream().map(Document::getId).toList()); - objectMapper.addMixIn(Document.class, ExerciseExportMixins.Document.class); - List exerciseTags = new ArrayList<>(exercise.getTags()); - // Objectives - List objectives = exercise.getObjectives(); - importExport.setObjectives(objectives); - objectMapper.addMixIn(Objective.class, ExerciseExportMixins.Objective.class); - // Lessons categories - List lessonsCategories = exercise.getLessonsCategories(); - importExport.setLessonsCategories(lessonsCategories); - objectMapper.addMixIn(LessonsCategory.class, ExerciseExportMixins.LessonsCategory.class); - // Lessons questions - List lessonsQuestions = lessonsCategories.stream() - .flatMap(category -> category.getQuestions().stream()).toList(); - importExport.setLessonsQuestions(lessonsQuestions); - objectMapper.addMixIn(LessonsQuestion.class, ExerciseExportMixins.LessonsQuestion.class); - // Audiences - List audiences = exercise.getAudiences(); - importExport.setAudiences(audiences); - objectMapper.addMixIn(Audience.class, - isWithPlayers ? ExerciseExportMixins.Audience.class : ExerciseExportMixins.EmptyAudience.class); - exerciseTags.addAll(audiences.stream().flatMap(audience -> audience.getTags().stream()).toList()); - if (isWithPlayers) { - // players - List players = audiences.stream().flatMap(audience -> audience.getUsers().stream()).distinct().toList(); - exerciseTags.addAll(players.stream().flatMap(user -> user.getTags().stream()).toList()); - importExport.setUsers(players); - objectMapper.addMixIn(User.class, ExerciseExportMixins.User.class); - // organizations - List organizations = players.stream().map(User::getOrganization).filter(Objects::nonNull).toList(); - exerciseTags.addAll(organizations.stream().flatMap(org -> org.getTags().stream()).toList()); - importExport.setOrganizations(organizations); - objectMapper.addMixIn(Organization.class, ExerciseExportMixins.Organization.class); - } - // Injects - List injects = exercise.getInjects(); - exerciseTags.addAll(injects.stream().flatMap(inject -> inject.getTags().stream()).toList()); - importExport.setInjects(injects); - objectMapper.addMixIn(Inject.class, ExerciseExportMixins.Inject.class); - // Documents - exerciseTags.addAll(exercise.getDocuments().stream().flatMap(doc -> doc.getTags().stream()).toList()); - // Articles / Medias - List
articles = exercise.getArticles(); - importExport.setArticles(articles); - objectMapper.addMixIn(Article.class, ExerciseExportMixins.Article.class); - List medias = articles.stream().map(Article::getMedia).distinct().toList(); - documentIds.addAll(medias.stream().flatMap(media -> media.getLogos().stream()).map(Document::getId).toList()); - importExport.setMedias(medias); - objectMapper.addMixIn(Media.class, ExerciseExportMixins.Media.class); - // Challenges - List challenges = fromIterable(challengeService.getExerciseChallenges(exerciseId)); - importExport.setChallenges(challenges); - documentIds.addAll( - challenges.stream().flatMap(challenge -> challenge.getDocuments().stream()).map(Document::getId).toList()); - objectMapper.addMixIn(Challenge.class, ExerciseExportMixins.Challenge.class); - exerciseTags.addAll(challenges.stream().flatMap(challenge -> challenge.getTags().stream()).toList()); - // Tags - importExport.setTags(exerciseTags.stream().distinct().toList()); - objectMapper.addMixIn(Tag.class, ExerciseExportMixins.Tag.class); - // -- Variables -- - List variables = this.variableService.variables(exerciseId); - importExport.setVariables(variables); - if (isWithVariableValues) { - objectMapper.addMixIn(Variable.class, VariableWithValueMixin.class); - } else { - objectMapper.addMixIn(Variable.class, VariableMixin.class); - } - // Build the response - String infos = "(" - + (isWithPlayers ? "with_players" : "no_players") - + " & " - + (isWithVariableValues ? "with_variable_values" : "no_variable_values") - + ")"; - String zipName = - (exercise.getName() + "_" + now().toString()) + "_" + infos + ".zip"; - response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + zipName); - response.addHeader(HttpHeaders.CONTENT_TYPE, "application/zip"); - response.setStatus(HttpServletResponse.SC_OK); - ZipOutputStream zipExport = new ZipOutputStream(response.getOutputStream()); - ZipEntry zipEntry = new ZipEntry(exercise.getName() + ".json"); - zipEntry.setComment(EXPORT_ENTRY_EXERCISE); - zipExport.putNextEntry(zipEntry); - zipExport.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(importExport)); - zipExport.closeEntry(); - // Add the documents - documentIds.stream().distinct().forEach(docId -> { - Document doc = documentRepository.findById(docId).orElseThrow(); - Optional docStream = fileService.getFile(doc); - if (docStream.isPresent()) { - try { - ZipEntry zipDoc = new ZipEntry(doc.getTarget()); - zipDoc.setComment(EXPORT_ENTRY_ATTACHMENT); - byte[] data = docStream.get().readAllBytes(); - zipExport.putNextEntry(zipDoc); - zipExport.write(data); - zipExport.closeEntry(); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, e.getMessage(), e); + if (grants.size() > 0) { + Iterable exerciseGrants = grantRepository.saveAll(grants); + exercise.setGrants(fromIterable(exerciseGrants)); + } + return exerciseRepository.save(exercise); + } + + @PutMapping("/api/exercises/{exerciseId}") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise updateExerciseInformation(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + exercise.setUpdateAttributes(input); + return exerciseRepository.save(exercise); + } + + @PutMapping("/api/exercises/{exerciseId}/start_date") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise updateExerciseStart(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateStartDateInput input) throws InputValidationException { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + if (!exercise.getStatus().equals(SCHEDULED)) { + String message = "Change date is only possible in scheduling state"; + throw new InputValidationException("exercise_start_date", message); + } + exercise.setUpdateAttributes(input); + return exerciseRepository.save(exercise); + } + + @PutMapping("/api/exercises/{exerciseId}/tags") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise updateExerciseTags(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateTagsInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + exercise.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); + return exerciseRepository.save(exercise); + } + + @PutMapping("/api/exercises/{exerciseId}/logos") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise updateExerciseLogos(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateLogoInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + exercise.setLogoDark(documentRepository.findById(input.getLogoDark()).orElse(null)); + exercise.setLogoLight(documentRepository.findById(input.getLogoLight()).orElse(null)); + return exerciseRepository.save(exercise); + } + + @PutMapping("/api/exercises/{exerciseId}/lessons") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise updateExerciseLessons(@PathVariable String exerciseId, @Valid @RequestBody ExerciseLessonsInput input) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + exercise.setLessonsAnonymized(input.getLessonsAnonymized()); + return exerciseRepository.save(exercise); + } + + @DeleteMapping("/api/exercises/{exerciseId}") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public void deleteExercise(@PathVariable String exerciseId) { + exerciseRepository.deleteById(exerciseId); + } + + @GetMapping("/api/exercises/{exerciseId}") + @PreAuthorize("isExerciseObserver(#exerciseId)") + public Exercise exercise(@PathVariable String exerciseId) { + return exerciseRepository.findById(exerciseId).orElseThrow(); + } + + @Transactional(rollbackOn = Exception.class) + @DeleteMapping("/api/exercises/{exerciseId}/{documentId}") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise deleteDocument(@PathVariable String exerciseId, @PathVariable String documentId) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + exercise.setUpdatedAt(now()); + Document doc = documentRepository.findById(documentId).orElseThrow(); + List docExercises = doc.getExercises().stream().filter(ex -> !ex.getId().equals(exerciseId)).toList(); + if (docExercises.isEmpty()) { + // Document is no longer associate to any exercise, delete it + documentRepository.delete(doc); + // All associations with this document will be automatically cleanup. + } else { + // Document associated to other exercise, cleanup + doc.setExercises(docExercises); + documentRepository.save(doc); + // Delete document from all exercise injects + injectService.cleanInjectsDocExercise(exerciseId, documentId); + } + return exerciseRepository.save(exercise); + } + + @Transactional(rollbackOn = Exception.class) + @PutMapping("/api/exercises/{exerciseId}/status") + @PreAuthorize("isExercisePlanner(#exerciseId)") + public Exercise changeExerciseStatus(@PathVariable String exerciseId, @Valid @RequestBody ExerciseUpdateStatusInput input) { + STATUS status = input.getStatus(); + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + // Check if next status is possible + List nextPossibleStatus = exercise.nextPossibleStatus(); + if (!nextPossibleStatus.contains(status)) { + throw new UnsupportedOperationException("Exercise cant support moving to status " + status.name()); + } + // In case of rescheduled of an exercise. + boolean isCloseState = CANCELED.equals(exercise.getStatus()) || FINISHED.equals(exercise.getStatus()); + if (isCloseState && SCHEDULED.equals(status)) { + exercise.setStart(null); + exercise.setEnd(null); + // Reset pauses + exercise.setCurrentPause(null); + pauseRepository.deleteAll(pauseRepository.findAllForExercise(exerciseId)); + // Reset injects outcome, communications and expectations + injectRepository.saveAll(injectRepository.findAllForExercise(exerciseId).stream().peek(Inject::clean).toList()); + // Reset lessons learned answers + List lessonsAnswers = lessonsCategoryRepository.findAll(LessonsCategorySpecification.fromExercise(exerciseId)).stream().flatMap(lessonsCategory -> lessonsQuestionRepository.findAll(LessonsQuestionSpecification.fromCategory(lessonsCategory.getId())).stream().flatMap(lessonsQuestion -> lessonsAnswerRepository.findAll(LessonsAnswerSpecification.fromQuestion(lessonsQuestion.getId())).stream())).toList(); + lessonsAnswerRepository.deleteAll(lessonsAnswers); + // Delete exercise transient files (communications, ...) + fileService.deleteDirectory(exerciseId); + } + // In case of manual start + if (SCHEDULED.equals(exercise.getStatus()) && RUNNING.equals(status)) { + Instant nextMinute = now().truncatedTo(MINUTES).plus(1, MINUTES); + exercise.setStart(nextMinute); + } + // If exercise move from pause to running state, + // we log the pause date to be able to recompute inject dates. + if (PAUSED.equals(exercise.getStatus()) && RUNNING.equals(status)) { + Instant lastPause = exercise.getCurrentPause().orElseThrow(); + exercise.setCurrentPause(null); + Pause pause = new Pause(); + pause.setDate(lastPause); + pause.setExercise(exercise); + pause.setDuration(between(lastPause, now()).getSeconds()); + pauseRepository.save(pause); + } + // If pause is asked, just set the pause date. + if (RUNNING.equals(exercise.getStatus()) && PAUSED.equals(status)) { + exercise.setCurrentPause(Instant.now()); } - } - }); - zipExport.finish(); - zipExport.close(); - } - - @PostMapping("/api/exercises/import") - @RolesAllowed(ROLE_ADMIN) - public void exerciseImport(@RequestPart("file") MultipartFile file) throws Exception { - importService.handleFileImport(file); - } - - @GetMapping("/api/player/exercises/{exerciseId}") - public PublicExercise playerExercise(@PathVariable String exerciseId, @RequestParam Optional userId) { - impersonateUser(userRepository, userId); // TODO Check Security - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - return new PublicExercise(exercise); - } - // endregion + // Cancelation + if (RUNNING.equals(exercise.getStatus()) && CANCELED.equals(status)) { + exercise.setEnd(now()); + } + exercise.setUpdatedAt(now()); + exercise.setStatus(status); + return exerciseRepository.save(exercise); + } + + @GetMapping("/api/exercises") + @RolesAllowed(ROLE_USER) + public List exercises() { + Iterable exercises = currentUser().isAdmin() ? exerciseRepository.findAll() : exerciseRepository.findAllGranted(currentUser().getId()); + return fromIterable(exercises).stream().map(ExerciseSimple::fromExercise).toList(); + } + // endregion + + // region communication + @GetMapping("/api/exercises/{exerciseId}/communications") + @PreAuthorize("isExerciseObserver(#exerciseId)") + public Iterable exerciseCommunications(@PathVariable String exerciseId) { + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + List communications = new ArrayList<>(); + exercise.getInjects().forEach(injectDoc -> communications.addAll(injectDoc.getCommunications())); + return communications; + } + + @GetMapping("/api/communications/attachment") + // @PreAuthorize("isExerciseObserver(#exerciseId)") + public void downloadAttachment(@RequestParam String file, HttpServletResponse response) throws IOException { + FileContainer fileContainer = fileService.getFileContainer(file).orElseThrow(); + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileContainer.getName()); + response.addHeader(HttpHeaders.CONTENT_TYPE, fileContainer.getContentType()); + response.setStatus(HttpServletResponse.SC_OK); + fileContainer.getInputStream().transferTo(response.getOutputStream()); + } + // endregion + + // region import/export + @GetMapping("/api/exercises/{exerciseId}/export") + @PreAuthorize("isExerciseObserver(#exerciseId)") + public void exerciseExport(@NotBlank @PathVariable final String exerciseId, @RequestParam(required = false) final boolean isWithPlayers, @RequestParam(required = false) final boolean isWithVariableValues, HttpServletResponse response) throws IOException { + // Setup the mapper for export + List documentIds = new ArrayList<>(); + ObjectMapper objectMapper = mapper.copy(); + if (!isWithPlayers) { + objectMapper.addMixIn(ExerciseFileExport.class, ExerciseExportMixins.ExerciseFileExport.class); + } + // Start exporting exercise + ExerciseFileExport importExport = new ExerciseFileExport(); + importExport.setVersion(1); + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + objectMapper.addMixIn(Exercise.class, ExerciseExportMixins.Exercise.class); + // Build the export + importExport.setExercise(exercise); + importExport.setDocuments(exercise.getDocuments()); + documentIds.addAll(exercise.getDocuments().stream().map(Document::getId).toList()); + objectMapper.addMixIn(Document.class, ExerciseExportMixins.Document.class); + List exerciseTags = new ArrayList<>(exercise.getTags()); + // Objectives + List objectives = exercise.getObjectives(); + importExport.setObjectives(objectives); + objectMapper.addMixIn(Objective.class, ExerciseExportMixins.Objective.class); + // Lessons categories + List lessonsCategories = exercise.getLessonsCategories(); + importExport.setLessonsCategories(lessonsCategories); + objectMapper.addMixIn(LessonsCategory.class, ExerciseExportMixins.LessonsCategory.class); + // Lessons questions + List lessonsQuestions = lessonsCategories.stream().flatMap(category -> category.getQuestions().stream()).toList(); + importExport.setLessonsQuestions(lessonsQuestions); + objectMapper.addMixIn(LessonsQuestion.class, ExerciseExportMixins.LessonsQuestion.class); + // Teams + List teams = exercise.getTeams(); + importExport.setTeams(teams); + objectMapper.addMixIn(Team.class, isWithPlayers ? ExerciseExportMixins.Team.class : ExerciseExportMixins.EmptyTeam.class); + exerciseTags.addAll(teams.stream().flatMap(team -> team.getTags().stream()).toList()); + if (isWithPlayers) { + // players + List players = teams.stream().flatMap(team -> team.getUsers().stream()).distinct().toList(); + exerciseTags.addAll(players.stream().flatMap(user -> user.getTags().stream()).toList()); + importExport.setUsers(players); + objectMapper.addMixIn(User.class, ExerciseExportMixins.User.class); + // organizations + List organizations = players.stream().map(User::getOrganization).filter(Objects::nonNull).toList(); + exerciseTags.addAll(organizations.stream().flatMap(org -> org.getTags().stream()).toList()); + importExport.setOrganizations(organizations); + objectMapper.addMixIn(Organization.class, ExerciseExportMixins.Organization.class); + } + // Injects + List injects = exercise.getInjects(); + exerciseTags.addAll(injects.stream().flatMap(inject -> inject.getTags().stream()).toList()); + importExport.setInjects(injects); + objectMapper.addMixIn(Inject.class, ExerciseExportMixins.Inject.class); + // Documents + exerciseTags.addAll(exercise.getDocuments().stream().flatMap(doc -> doc.getTags().stream()).toList()); + // Articles / Medias + List
articles = exercise.getArticles(); + importExport.setArticles(articles); + objectMapper.addMixIn(Article.class, ExerciseExportMixins.Article.class); + List medias = articles.stream().map(Article::getMedia).distinct().toList(); + documentIds.addAll(medias.stream().flatMap(media -> media.getLogos().stream()).map(Document::getId).toList()); + importExport.setMedias(medias); + objectMapper.addMixIn(Media.class, ExerciseExportMixins.Media.class); + // Challenges + List challenges = fromIterable(challengeService.getExerciseChallenges(exerciseId)); + importExport.setChallenges(challenges); + documentIds.addAll(challenges.stream().flatMap(challenge -> challenge.getDocuments().stream()).map(Document::getId).toList()); + objectMapper.addMixIn(Challenge.class, ExerciseExportMixins.Challenge.class); + exerciseTags.addAll(challenges.stream().flatMap(challenge -> challenge.getTags().stream()).toList()); + // Tags + importExport.setTags(exerciseTags.stream().distinct().toList()); + objectMapper.addMixIn(Tag.class, ExerciseExportMixins.Tag.class); + // -- Variables -- + List variables = this.variableService.variables(exerciseId); + importExport.setVariables(variables); + if (isWithVariableValues) { + objectMapper.addMixIn(Variable.class, VariableWithValueMixin.class); + } else { + objectMapper.addMixIn(Variable.class, VariableMixin.class); + } + // Build the response + String infos = "(" + (isWithPlayers ? "with_players" : "no_players") + " & " + (isWithVariableValues ? "with_variable_values" : "no_variable_values") + ")"; + String zipName = (exercise.getName() + "_" + now().toString()) + "_" + infos + ".zip"; + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + zipName); + response.addHeader(HttpHeaders.CONTENT_TYPE, "application/zip"); + response.setStatus(HttpServletResponse.SC_OK); + ZipOutputStream zipExport = new ZipOutputStream(response.getOutputStream()); + ZipEntry zipEntry = new ZipEntry(exercise.getName() + ".json"); + zipEntry.setComment(EXPORT_ENTRY_EXERCISE); + zipExport.putNextEntry(zipEntry); + zipExport.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(importExport)); + zipExport.closeEntry(); + // Add the documents + documentIds.stream().distinct().forEach(docId -> { + Document doc = documentRepository.findById(docId).orElseThrow(); + Optional docStream = fileService.getFile(doc); + if (docStream.isPresent()) { + try { + ZipEntry zipDoc = new ZipEntry(doc.getTarget()); + zipDoc.setComment(EXPORT_ENTRY_ATTACHMENT); + byte[] data = docStream.get().readAllBytes(); + zipExport.putNextEntry(zipDoc); + zipExport.write(data); + zipExport.closeEntry(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + } + } + }); + zipExport.finish(); + zipExport.close(); + } + + @PostMapping("/api/exercises/import") + @RolesAllowed(ROLE_ADMIN) + public void exerciseImport(@RequestPart("file") MultipartFile file) throws Exception { + importService.handleFileImport(file); + } + + @GetMapping("/api/player/exercises/{exerciseId}") + public PublicExercise playerExercise(@PathVariable String exerciseId, @RequestParam Optional userId) { + impersonateUser(userRepository, userId); // TODO Check Security + Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + return new PublicExercise(exercise); + } + // endregion } diff --git a/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseExportMixins.java b/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseExportMixins.java index 1c81b16ef3..4ec5643ac4 100644 --- a/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseExportMixins.java +++ b/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseExportMixins.java @@ -44,22 +44,22 @@ public static class Organization { } @JsonIncludeProperties(value = { - "audience_id", - "audience_name", - "audience_description", - "audience_tags", - "audience_users", + "team_id", + "team_name", + "team_description", + "team_tags", + "team_users", }) - public static class Audience { + public static class Team { } @JsonIncludeProperties(value = { - "audience_id", - "audience_name", - "audience_description", - "audience_tags", + "team_id", + "team_name", + "team_description", + "team_tags", }) - public static class EmptyAudience { + public static class EmptyTeam { } @JsonIncludeProperties(value = { @@ -70,12 +70,12 @@ public static class EmptyAudience { "inject_city", "inject_type", "inject_contract", - "inject_all_audiences", + "inject_all_teams", "inject_depends_on", "inject_depends_duration", "inject_tags", "inject_documents", - "inject_audiences", + "inject_teams", "inject_content", }) public static class Inject { @@ -171,7 +171,7 @@ public static class Challenge { "lessons_category_description", "lessons_category_order", "lessons_category_questions", - "lessons_category_audiences", + "lessons_category_teams", }) public static class LessonsCategory { } diff --git a/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseFileExport.java b/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseFileExport.java index 7db8ed0414..0941b035f6 100644 --- a/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseFileExport.java +++ b/openex-api/src/main/java/io/openex/rest/exercise/exports/ExerciseFileExport.java @@ -24,7 +24,7 @@ public class ExerciseFileExport { private Exercise exercise; @JsonProperty("exercise_audiences") - private List audiences = new ArrayList<>(); + private List teams = new ArrayList<>(); @JsonProperty("exercise_objectives") private List objectives = new ArrayList<>(); diff --git a/openex-api/src/main/java/io/openex/rest/exercise/form/ExerciseTeamPlayersEnableInput.java b/openex-api/src/main/java/io/openex/rest/exercise/form/ExerciseTeamPlayersEnableInput.java new file mode 100644 index 0000000000..1dabec3425 --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/exercise/form/ExerciseTeamPlayersEnableInput.java @@ -0,0 +1,22 @@ +package io.openex.rest.exercise.form; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +import static io.openex.config.AppConfig.MANDATORY_MESSAGE; + +public class ExerciseTeamPlayersEnableInput { + @JsonProperty("exercise_team_players") + private List playersIds = new ArrayList<>(); + + public List getPlayersIds() { + return playersIds; + } + + public void setPlayersIds(List playersIds) { + this.playersIds = playersIds; + } +} diff --git a/openex-api/src/main/java/io/openex/rest/exercise/form/ExerciseUpdateTeamsInput.java b/openex-api/src/main/java/io/openex/rest/exercise/form/ExerciseUpdateTeamsInput.java new file mode 100644 index 0000000000..f3b4f2a9bf --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/exercise/form/ExerciseUpdateTeamsInput.java @@ -0,0 +1,20 @@ +package io.openex.rest.exercise.form; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class ExerciseUpdateTeamsInput { + + @JsonProperty("exercise_teams") + private List teamIds = new ArrayList<>(); + + public List getTeamIds() { + return teamIds; + } + + public void setTeamIds(List teamIds) { + this.teamIds = teamIds; + } +} diff --git a/openex-api/src/main/java/io/openex/rest/inject/InjectApi.java b/openex-api/src/main/java/io/openex/rest/inject/InjectApi.java index 83edd43bcd..e3dfcb28f0 100644 --- a/openex-api/src/main/java/io/openex/rest/inject/InjectApi.java +++ b/openex-api/src/main/java/io/openex/rest/inject/InjectApi.java @@ -43,7 +43,7 @@ public class InjectApi extends RestBehavior { private UserRepository userRepository; private InjectRepository injectRepository; private InjectDocumentRepository injectDocumentRepository; - private AudienceRepository audienceRepository; + private TeamRepository teamRepository; private TagRepository tagRepository; private DocumentRepository documentRepository; private ApplicationContext context; @@ -81,8 +81,8 @@ public void setExerciseRepository(ExerciseRepository exerciseRepository) { } @Autowired - public void setAudienceRepository(AudienceRepository audienceRepository) { - this.audienceRepository = audienceRepository; + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; } @Autowired @@ -138,7 +138,7 @@ public Inject updateInject( inject.setUpdateAttributes(input); // Set dependencies inject.setDependsOn(updateRelation(input.getDependsOn(), inject.getDependsOn(), injectRepository)); - inject.setAudiences(fromIterable(audienceRepository.findAllById(input.getAudiences()))); + inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); List documents = input.getDocuments(); List askedDocumentIds = documents.stream().map(InjectDocumentInput::getDocumentId).toList(); @@ -194,10 +194,10 @@ public Inject exerciseInject(@PathVariable String exerciseId, @PathVariable Stri return injectRepository.findById(injectId).orElseThrow(); } - @GetMapping("/api/exercises/{exerciseId}/injects/{injectId}/audiences") + @GetMapping("/api/exercises/{exerciseId}/injects/{injectId}/teams") @PreAuthorize("isExerciseObserver(#exerciseId)") - public Iterable exerciseInjectAudiences(@PathVariable String exerciseId, @PathVariable String injectId) { - return injectRepository.findById(injectId).orElseThrow().getAudiences(); + public Iterable exerciseInjectTeams(@PathVariable String exerciseId, @PathVariable String injectId) { + return injectRepository.findById(injectId).orElseThrow().getTeams(); } @GetMapping("/api/exercises/{exerciseId}/injects/{injectId}/communications") @@ -221,7 +221,7 @@ public Inject createInject(@PathVariable String exerciseId, @Valid @RequestBody inject.setExercise(exercise); // Set dependencies inject.setDependsOn(resolveOptionalRelation(input.getDependsOn(), injectRepository)); - inject.setAudiences(fromIterable(audienceRepository.findAllById(input.getAudiences()))); + inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); List injectDocuments = input.getDocuments().stream() .map(i -> { @@ -307,13 +307,13 @@ public Inject setInjectStatus(@PathVariable String exerciseId, @PathVariable Str return injectRepository.save(inject); } - @PutMapping("/api/exercises/{exerciseId}/injects/{injectId}/audiences") + @PutMapping("/api/exercises/{exerciseId}/injects/{injectId}/teams") @PreAuthorize("isExercisePlanner(#exerciseId)") - public Inject updateInjectAudiences(@PathVariable String exerciseId, @PathVariable String injectId, - @Valid @RequestBody InjectAudiencesInput input) { + public Inject updateInjectTeams(@PathVariable String exerciseId, @PathVariable String injectId, + @Valid @RequestBody InjectTeamsInput input) { Inject inject = injectRepository.findById(injectId).orElseThrow(); - Iterable injectAudiences = audienceRepository.findAllById(input.getAudienceIds()); - inject.setAudiences(fromIterable(injectAudiences)); + Iterable injectTeams = teamRepository.findAllById(input.getTeamIds()); + inject.setTeams(fromIterable(injectTeams)); return injectRepository.save(inject); } diff --git a/openex-api/src/main/java/io/openex/rest/inject/form/InjectAudiencesInput.java b/openex-api/src/main/java/io/openex/rest/inject/form/InjectAudiencesInput.java deleted file mode 100644 index cf18f8d396..0000000000 --- a/openex-api/src/main/java/io/openex/rest/inject/form/InjectAudiencesInput.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.openex.rest.inject.form; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public class InjectAudiencesInput { - - @JsonProperty("inject_audiences") - private List audienceIds; - - public List getAudienceIds() { - return audienceIds; - } - - public void setAudienceIds(List audienceIds) { - this.audienceIds = audienceIds; - } -} diff --git a/openex-api/src/main/java/io/openex/rest/inject/form/InjectInput.java b/openex-api/src/main/java/io/openex/rest/inject/form/InjectInput.java index e2519430c1..137251c13a 100644 --- a/openex-api/src/main/java/io/openex/rest/inject/form/InjectInput.java +++ b/openex-api/src/main/java/io/openex/rest/inject/form/InjectInput.java @@ -31,14 +31,14 @@ public class InjectInput { @JsonProperty("inject_depends_duration") private Long dependsDuration; - @JsonProperty("inject_audiences") - private List audiences = new ArrayList<>(); + @JsonProperty("inject_teams") + private List teams = new ArrayList<>(); @JsonProperty("inject_documents") private List documents = new ArrayList<>(); - @JsonProperty("inject_all_audiences") - private boolean allAudiences = false; + @JsonProperty("inject_all_teams") + private boolean allTeams = false; @JsonProperty("inject_country") private String country; @@ -49,8 +49,96 @@ public class InjectInput { @JsonProperty("inject_tags") private List tagIds = new ArrayList<>(); - public boolean getAllAudiences() { - return allAudiences; + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public ObjectNode getContent() { + return content; + } + + public void setContent(ObjectNode content) { + this.content = content; + } + + public String getContract() { + return contract; + } + + public void setContract(String contract) { + this.contract = contract; + } + + public List getTeams() { + return teams; + } + + public void setTeams(List teams) { + this.teams = teams; + } + + public boolean getAllTeams() { + return allTeams; + } + + public void setAllTeams(boolean allTeams) { + this.allTeams = allTeams; + } + + public boolean isAllTeams() { + return allTeams; + } + + public List getDocuments() { + return documents; + } + + public void setDocuments(List documents) { + this.documents = documents; + } + + public String getDependsOn() { + return dependsOn; + } + + public void setDependsOn(String dependsOn) { + this.dependsOn = dependsOn; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public List getTagIds() { + return tagIds; + } + + public void setTagIds(List tagIds) { + this.tagIds = tagIds; } public Inject toInject() { @@ -60,7 +148,7 @@ public Inject toInject() { inject.setContent(getContent()); inject.setContract(getContract()); inject.setDependsDuration(getDependsDuration()); - inject.setAllAudiences(getAllAudiences()); + inject.setAllTeams(getAllTeams()); inject.setCountry(getCountry()); inject.setCity(getCity()); return inject; diff --git a/openex-api/src/main/java/io/openex/rest/inject/form/InjectTeamsInput.java b/openex-api/src/main/java/io/openex/rest/inject/form/InjectTeamsInput.java new file mode 100644 index 0000000000..142e7409bf --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/inject/form/InjectTeamsInput.java @@ -0,0 +1,19 @@ +package io.openex.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class InjectTeamsInput { + + @JsonProperty("inject_teams") + private List teamIds; + + public List getTeamIds() { + return teamIds; + } + + public void setTeamIds(List teamIds) { + this.teamIds = teamIds; + } +} diff --git a/openex-api/src/main/java/io/openex/rest/lessons/LessonsApi.java b/openex-api/src/main/java/io/openex/rest/lessons/LessonsApi.java index 10c274a947..c840d2ae72 100644 --- a/openex-api/src/main/java/io/openex/rest/lessons/LessonsApi.java +++ b/openex-api/src/main/java/io/openex/rest/lessons/LessonsApi.java @@ -23,7 +23,7 @@ public class LessonsApi extends RestBehavior { private ExerciseRepository exerciseRepository; - private AudienceRepository audienceRepository; + private TeamRepository teamRepository; private LessonsTemplateRepository lessonsTemplateRepository; private LessonsCategoryRepository lessonsCategoryRepository; private LessonsQuestionRepository lessonsQuestionRepository; @@ -47,8 +47,8 @@ public void setExerciseRepository(ExerciseRepository exerciseRepository) { } @Autowired - public void setAudienceRepository(AudienceRepository audienceRepository) { - this.audienceRepository = audienceRepository; + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; } @Autowired @@ -159,11 +159,11 @@ public void deleteExerciseLessonsCategory(@PathVariable String exerciseId, @Path @PutMapping("/api/exercises/{exerciseId}/lessons_categories/{lessonsCategoryId}/audiences") @PreAuthorize("isExercisePlanner(#exerciseId)") - public LessonsCategory updateExerciseLessonsCategoryAudiences(@PathVariable String exerciseId, - @PathVariable String lessonsCategoryId, @Valid @RequestBody LessonsCategoryAudiencesInput input) { + public LessonsCategory updateExerciseLessonsCategoryTeams(@PathVariable String exerciseId, + @PathVariable String lessonsCategoryId, @Valid @RequestBody LessonsCategoryTeamsInput input) { LessonsCategory lessonsCategory = lessonsCategoryRepository.findById(lessonsCategoryId).orElseThrow(); - Iterable lessonsCategoryAudiences = audienceRepository.findAllById(input.getAudienceIds()); - lessonsCategory.setAudiences(fromIterable(lessonsCategoryAudiences)); + Iterable lessonsCategoryTeams = teamRepository.findAllById(input.getTeamIds()); + lessonsCategory.setTeams(fromIterable(lessonsCategoryTeams)); return lessonsCategoryRepository.save(lessonsCategory); } @@ -215,8 +215,8 @@ public void sendExerciseLessons(@PathVariable String exerciseId, @Valid @Request Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); List lessonsCategories = lessonsCategoryRepository.findAll( LessonsCategorySpecification.fromExercise(exerciseId)).stream().toList(); - List users = lessonsCategories.stream().flatMap(lessonsCategory -> lessonsCategory.getAudiences().stream() - .flatMap(audience -> audience.getUsers().stream())).distinct().toList(); + List users = lessonsCategories.stream().flatMap(lessonsCategory -> lessonsCategory.getTeams().stream() + .flatMap(team -> team.getUsers().stream())).distinct().toList(); mailingService.sendEmail(input.getSubject(), input.getBody(), users, Optional.of(exercise)); } diff --git a/openex-api/src/main/java/io/openex/rest/lessons/form/LessonsCategoryAudiencesInput.java b/openex-api/src/main/java/io/openex/rest/lessons/form/LessonsCategoryAudiencesInput.java deleted file mode 100644 index 6d22ef5f92..0000000000 --- a/openex-api/src/main/java/io/openex/rest/lessons/form/LessonsCategoryAudiencesInput.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.openex.rest.lessons.form; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public class LessonsCategoryAudiencesInput { - - @JsonProperty("lessons_category_audiences") - private List audienceIds; - - public List getAudienceIds() { - return audienceIds; - } - - public void setAudienceIds(List audienceIds) { - this.audienceIds = audienceIds; - } -} diff --git a/openex-api/src/main/java/io/openex/rest/lessons/form/LessonsCategoryTeamsInput.java b/openex-api/src/main/java/io/openex/rest/lessons/form/LessonsCategoryTeamsInput.java new file mode 100644 index 0000000000..52b27043b2 --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/lessons/form/LessonsCategoryTeamsInput.java @@ -0,0 +1,19 @@ +package io.openex.rest.lessons.form; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class LessonsCategoryTeamsInput { + + @JsonProperty("lessons_category_teams") + private List teamIds; + + public List getTeamIds() { + return teamIds; + } + + public void setTeamIds(List teamIds) { + this.teamIds = teamIds; + } +} diff --git a/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java b/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java index 6c190940d2..e02f4c160b 100644 --- a/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java +++ b/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java @@ -71,7 +71,7 @@ public boolean isExercisePlayer(String exerciseId) { return true; } Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); - List players = exercise.getPlayers(); + List players = exercise.getUsers(); Optional player = players.stream() .filter(user -> user.getId().equals(getUser().getId())).findAny(); return player.isPresent(); diff --git a/openex-api/src/main/java/io/openex/rest/team/TeamApi.java b/openex-api/src/main/java/io/openex/rest/team/TeamApi.java new file mode 100644 index 0000000000..764506c677 --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/team/TeamApi.java @@ -0,0 +1,120 @@ +package io.openex.rest.team; + +import io.openex.config.OpenexPrincipal; +import io.openex.database.model.*; +import io.openex.database.repository.*; +import io.openex.rest.team.form.TeamCreateInput; +import io.openex.rest.team.form.TeamUpdateActivationInput; +import io.openex.rest.team.form.TeamUpdateInput; +import io.openex.rest.team.form.UpdateUsersTeamInput; +import io.openex.rest.helper.RestBehavior; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.security.RolesAllowed; +import javax.validation.Valid; + +import java.util.ArrayList; +import java.util.List; + +import static io.openex.config.SessionHelper.currentUser; +import static io.openex.database.model.User.ROLE_USER; +import static io.openex.helper.DatabaseHelper.updateRelation; +import static io.openex.helper.StreamHelper.fromIterable; +import static java.time.Instant.now; + +@RestController +@RolesAllowed(ROLE_USER) +public class TeamApi extends RestBehavior { + private TeamRepository teamRepository; + private UserRepository userRepository; + private OrganizationRepository organizationRepository; + private TagRepository tagRepository; + + @Autowired + public void setUserRepository(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Autowired + public void setTeamRepository(TeamRepository teamRepository) { + this.teamRepository = teamRepository; + } + + @Autowired + public void setOrganizationRepository(OrganizationRepository organizationRepository) { + this.organizationRepository = organizationRepository; + } + + @Autowired + public void setTagRepository(TagRepository tagRepository) { + this.tagRepository = tagRepository; + } + + @GetMapping("/api/teams") + @PreAuthorize("isObserver()") + public Iterable getTeams() { + List teams; + OpenexPrincipal currentUser = currentUser(); + if (currentUser.isAdmin()) { + teams = fromIterable(teamRepository.findAll()); + } else { + User local = userRepository.findById(currentUser.getId()).orElseThrow(); + List organizationIds = local.getGroups().stream() + .flatMap(group -> group.getOrganizations().stream()) + .map(Organization::getId) + .toList(); + teams = teamRepository.teamsAccessibleFromOrganizations(organizationIds); + } + return teams; + } + + @GetMapping("/api/teams/{teamId}") + @PreAuthorize("isObserver()") + public Team getTeam(@PathVariable String teamId) { + return teamRepository.findById(teamId).orElseThrow(); + } + + @GetMapping("/api/teams/{teamId}/players") + @PreAuthorize("isObserver()") + public Iterable getTeamPlayers(@PathVariable String teamId) { + return teamRepository.findById(teamId).orElseThrow().getUsers(); + } + + @PostMapping("/api/teams") + @PreAuthorize("isPlanner()") + public Team createTeam(@Valid @RequestBody TeamCreateInput input) { + Team team = new Team(); + team.setUpdateAttributes(input); + team.setOrganization(updateRelation(input.getOrganizationId(), team.getOrganization(), organizationRepository)); + team.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); + return teamRepository.save(team); + } + + @DeleteMapping("/api/teams/{teamId}") + @PreAuthorize("isPlanner()") + public void deleteTeam(@PathVariable String teamId) { + teamRepository.deleteById(teamId); + } + + @PutMapping("/api/teams/{teamId}") + @PreAuthorize("isPlanner()") + public Team updateTeam(@PathVariable String teamId, @Valid @RequestBody TeamUpdateInput input) { + Team team = teamRepository.findById(teamId).orElseThrow(); + team.setUpdateAttributes(input); + team.setUpdatedAt(now()); + team.setTags(fromIterable(tagRepository.findAllById(input.getTagIds()))); + team.setOrganization(updateRelation(input.getOrganizationId(), team.getOrganization(), organizationRepository)); + return teamRepository.save(team); + } + + @PutMapping("/api/teams/{teamId}/players") + @PreAuthorize("isPlanner()") + public Team updateTeamUsers(@PathVariable String teamId, @Valid @RequestBody UpdateUsersTeamInput input) { + Team team = teamRepository.findById(teamId).orElseThrow(); + Iterable teamUsers = userRepository.findAllById(input.getUserIds()); + team.setUsers(fromIterable(teamUsers)); + return teamRepository.save(team); + } +} diff --git a/openex-api/src/main/java/io/openex/rest/audience/form/AudienceUpdateInput.java b/openex-api/src/main/java/io/openex/rest/team/form/TeamCreateInput.java similarity index 65% rename from openex-api/src/main/java/io/openex/rest/audience/form/AudienceUpdateInput.java rename to openex-api/src/main/java/io/openex/rest/team/form/TeamCreateInput.java index 88771961d0..01cd47caab 100644 --- a/openex-api/src/main/java/io/openex/rest/audience/form/AudienceUpdateInput.java +++ b/openex-api/src/main/java/io/openex/rest/team/form/TeamCreateInput.java @@ -1,23 +1,27 @@ -package io.openex.rest.audience.form; +package io.openex.rest.team.form; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotBlank; + import java.util.ArrayList; import java.util.List; import static io.openex.config.AppConfig.MANDATORY_MESSAGE; -public class AudienceUpdateInput { +public class TeamCreateInput { @NotBlank(message = MANDATORY_MESSAGE) - @JsonProperty("audience_name") + @JsonProperty("team_name") private String name; - @JsonProperty("audience_description") + @JsonProperty("team_description") private String description; - @JsonProperty("audience_tags") + @JsonProperty("team_organization") + private String organizationId; + + @JsonProperty("team_tags") private List tagIds = new ArrayList<>(); public String getName() { @@ -36,6 +40,14 @@ public void setDescription(String description) { this.description = description; } + public String getOrganizationId() { + return organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + public List getTagIds() { return tagIds; } diff --git a/openex-api/src/main/java/io/openex/rest/audience/form/AudienceUpdateActivationInput.java b/openex-api/src/main/java/io/openex/rest/team/form/TeamUpdateActivationInput.java similarity index 66% rename from openex-api/src/main/java/io/openex/rest/audience/form/AudienceUpdateActivationInput.java rename to openex-api/src/main/java/io/openex/rest/team/form/TeamUpdateActivationInput.java index 7784fcb618..19ccc8c4cb 100644 --- a/openex-api/src/main/java/io/openex/rest/audience/form/AudienceUpdateActivationInput.java +++ b/openex-api/src/main/java/io/openex/rest/team/form/TeamUpdateActivationInput.java @@ -1,10 +1,10 @@ -package io.openex.rest.audience.form; +package io.openex.rest.team.form; import com.fasterxml.jackson.annotation.JsonProperty; -public class AudienceUpdateActivationInput { +public class TeamUpdateActivationInput { - @JsonProperty("audience_enabled") + @JsonProperty("team_enabled") private boolean enabled; public boolean isEnabled() { diff --git a/openex-api/src/main/java/io/openex/rest/audience/form/AudienceCreateInput.java b/openex-api/src/main/java/io/openex/rest/team/form/TeamUpdateInput.java similarity index 65% rename from openex-api/src/main/java/io/openex/rest/audience/form/AudienceCreateInput.java rename to openex-api/src/main/java/io/openex/rest/team/form/TeamUpdateInput.java index 1c48863983..a2dd9cd104 100644 --- a/openex-api/src/main/java/io/openex/rest/audience/form/AudienceCreateInput.java +++ b/openex-api/src/main/java/io/openex/rest/team/form/TeamUpdateInput.java @@ -1,24 +1,26 @@ -package io.openex.rest.audience.form; +package io.openex.rest.team.form; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotBlank; - import java.util.ArrayList; import java.util.List; import static io.openex.config.AppConfig.MANDATORY_MESSAGE; -public class AudienceCreateInput { +public class TeamUpdateInput { @NotBlank(message = MANDATORY_MESSAGE) - @JsonProperty("audience_name") + @JsonProperty("team_name") private String name; - @JsonProperty("audience_description") + @JsonProperty("team_description") private String description; - @JsonProperty("audience_tags") + @JsonProperty("team_organization") + private String organizationId; + + @JsonProperty("team_tags") private List tagIds = new ArrayList<>(); public String getName() { @@ -37,6 +39,14 @@ public void setDescription(String description) { this.description = description; } + public String getOrganizationId() { + return organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + public List getTagIds() { return tagIds; } diff --git a/openex-api/src/main/java/io/openex/rest/audience/form/UpdateUsersAudienceInput.java b/openex-api/src/main/java/io/openex/rest/team/form/UpdateUsersTeamInput.java similarity index 70% rename from openex-api/src/main/java/io/openex/rest/audience/form/UpdateUsersAudienceInput.java rename to openex-api/src/main/java/io/openex/rest/team/form/UpdateUsersTeamInput.java index d81a1c5aab..575b2c0961 100644 --- a/openex-api/src/main/java/io/openex/rest/audience/form/UpdateUsersAudienceInput.java +++ b/openex-api/src/main/java/io/openex/rest/team/form/UpdateUsersTeamInput.java @@ -1,12 +1,12 @@ -package io.openex.rest.audience.form; +package io.openex.rest.team.form; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -public class UpdateUsersAudienceInput { +public class UpdateUsersTeamInput { - @JsonProperty("audience_users") + @JsonProperty("team_users") private List userIds; public List getUserIds() { diff --git a/openex-api/src/test/java/io/openex/helper/InjectHelperTest.java b/openex-api/src/test/java/io/openex/helper/InjectHelperTest.java index 9578b4f127..1012388c30 100644 --- a/openex-api/src/test/java/io/openex/helper/InjectHelperTest.java +++ b/openex-api/src/test/java/io/openex/helper/InjectHelperTest.java @@ -1,10 +1,7 @@ package io.openex.helper; import io.openex.database.model.*; -import io.openex.database.repository.AudienceRepository; -import io.openex.database.repository.ExerciseRepository; -import io.openex.database.repository.InjectRepository; -import io.openex.database.repository.UserRepository; +import io.openex.database.repository.*; import io.openex.execution.ExecutableInject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,6 +9,8 @@ import org.springframework.boot.test.context.SpringBootTest; import java.time.Instant; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import static io.openex.database.model.Exercise.STATUS.RUNNING; @@ -26,11 +25,14 @@ public class InjectHelperTest { private InjectHelper injectHelper; @Autowired - private AudienceRepository audienceRepository; + private TeamRepository teamRepository; @Autowired private ExerciseRepository exerciseRepository; + @Autowired + private ExerciseTeamUserRepository exerciseTeamUserRepository; + @Autowired private InjectRepository injectRepository; @@ -46,17 +48,23 @@ void injectsToRunTest() { exercise.setStart(Instant.now()); exercise.setStatus(RUNNING); Exercise exerciseSaved = this.exerciseRepository.save(exercise); - + List exercises = new ArrayList<>(); + exercises.add(exerciseSaved); User user = new User(); user.setEmail(USER_EMAIL); this.userRepository.save(user); - Audience audience = new Audience(); - audience.setName("My audience"); - audience.setEnabled(true); - audience.setUsers(List.of(user)); - audience.setExercise(exerciseSaved); - this.audienceRepository.save(audience); + Team team = new Team(); + team.setName("My team"); + team.setExercises(exercises); + team.setUsers(List.of(user)); + this.teamRepository.save(team); + + ExerciseTeamUser exerciseTeamUser = new ExerciseTeamUser(); + exerciseTeamUser.setExercise(exercise); + exerciseTeamUser.setTeam(team); + exerciseTeamUser.setUser(user); + this.exerciseTeamUserRepository.save(exerciseTeamUser); // Executable Inject Inject inject = new Inject(); @@ -64,7 +72,7 @@ void injectsToRunTest() { InjectStatus status = new InjectStatus(); inject.setStatus(status); inject.setExercise(exerciseSaved); - inject.setAudiences(List.of(audience)); + inject.setTeams(List.of(team)); inject.setDependsDuration(0L); this.injectRepository.save(inject); @@ -74,7 +82,7 @@ void injectsToRunTest() { // -- ASSERT -- assertFalse(executableInjects.isEmpty()); ExecutableInject executableInject = executableInjects.get(0); - assertEquals(1, executableInject.getAudienceSize()); + assertEquals(1, executableInject.getTeamSize()); assertEquals(1, executableInject.getUsers().size()); assertEquals(USER_EMAIL, executableInject.getUsers().get(0).getUser().getEmail()); } diff --git a/openex-framework/src/main/java/io/openex/contract/Contract.java b/openex-framework/src/main/java/io/openex/contract/Contract.java index ae9f68dca7..73349ca24c 100644 --- a/openex-framework/src/main/java/io/openex/contract/Contract.java +++ b/openex-framework/src/main/java/io/openex/contract/Contract.java @@ -60,8 +60,8 @@ private Contract( this.variables.add(VariableHelper.userVariable); // Exercise variables this.variables.add(VariableHelper.exerciceVariable); - // Audiences - this.variables.add(VariableHelper.audienceVariable); + // Teams + this.variables.add(VariableHelper.teamVariable); // Direct uris this.variables.addAll(VariableHelper.uriVariables); } diff --git a/openex-framework/src/main/java/io/openex/contract/ContractType.java b/openex-framework/src/main/java/io/openex/contract/ContractType.java index ea0ea91ad5..6cf9b8a3c1 100644 --- a/openex-framework/src/main/java/io/openex/contract/ContractType.java +++ b/openex-framework/src/main/java/io/openex/contract/ContractType.java @@ -23,8 +23,8 @@ public enum ContractType { DependencySelect, @JsonProperty("attachment") Attachment, - @JsonProperty("audience") - Audience, + @JsonProperty("team") + Team, @JsonProperty("expectation") Expectation } diff --git a/openex-framework/src/main/java/io/openex/contract/fields/ContractAudience.java b/openex-framework/src/main/java/io/openex/contract/fields/ContractAudience.java deleted file mode 100644 index b29d20e3c4..0000000000 --- a/openex-framework/src/main/java/io/openex/contract/fields/ContractAudience.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.openex.contract.fields; - -import io.openex.contract.ContractCardinality; -import io.openex.contract.ContractType; - -public class ContractAudience extends ContractCardinalityElement { - - public ContractAudience(String key, String label, ContractCardinality cardinality) { - super(key, label, cardinality); - } - - public static ContractAudience audienceField(String key, String label, ContractCardinality cardinality) { - return new ContractAudience(key, label, cardinality); - } - - @Override - public ContractType getType() { - return ContractType.Audience; - } -} diff --git a/openex-framework/src/main/java/io/openex/contract/fields/ContractTeam.java b/openex-framework/src/main/java/io/openex/contract/fields/ContractTeam.java new file mode 100644 index 0000000000..f4a2fd7ca0 --- /dev/null +++ b/openex-framework/src/main/java/io/openex/contract/fields/ContractTeam.java @@ -0,0 +1,20 @@ +package io.openex.contract.fields; + +import io.openex.contract.ContractCardinality; +import io.openex.contract.ContractType; + +public class ContractTeam extends ContractCardinalityElement { + + public ContractTeam(String key, String label, ContractCardinality cardinality) { + super(key, label, cardinality); + } + + public static ContractTeam teamField(String key, String label, ContractCardinality cardinality) { + return new ContractTeam(key, label, cardinality); + } + + @Override + public ContractType getType() { + return ContractType.Team; + } +} diff --git a/openex-framework/src/main/java/io/openex/contract/variables/VariableHelper.java b/openex-framework/src/main/java/io/openex/contract/variables/VariableHelper.java index 0403ae2b13..e277f44207 100644 --- a/openex-framework/src/main/java/io/openex/contract/variables/VariableHelper.java +++ b/openex-framework/src/main/java/io/openex/contract/variables/VariableHelper.java @@ -13,7 +13,7 @@ public class VariableHelper { public static final String USER = "user"; public static final String EXERCISE = "exercise"; - public static final String AUDIENCES = "audiences"; + public static final String TEAMS = "audiences"; public static final String COMCHECK = "comcheck"; public static final String PLAYER_URI = "player_uri"; public static final String CHALLENGES_URI = "challenges_uri"; @@ -36,7 +36,7 @@ public class VariableHelper { variable(EXERCISE + ".description", "Description of the exercise", VariableType.String, One) )); - public static final ContractVariable audienceVariable = variable(AUDIENCES, "List of audience name for the injection", + public static final ContractVariable teamVariable = variable(TEAMS, "List of audience name for the injection", VariableType.String, Multiple); public static final List uriVariables = List.of( diff --git a/openex-framework/src/main/java/io/openex/execution/ExecutableInject.java b/openex-framework/src/main/java/io/openex/execution/ExecutableInject.java index d6913f04e3..cc3be4ac28 100644 --- a/openex-framework/src/main/java/io/openex/execution/ExecutableInject.java +++ b/openex-framework/src/main/java/io/openex/execution/ExecutableInject.java @@ -1,7 +1,7 @@ package io.openex.execution; import io.openex.contract.Contract; -import io.openex.database.model.Audience; +import io.openex.database.model.Team; import io.openex.database.model.Inject; import io.openex.database.model.Injection; import org.springframework.web.multipart.MultipartFile; @@ -14,27 +14,27 @@ public class ExecutableInject { private final Contract contract; private final Injection source; private final List users; - private final List audiences; + private final List teams; private final boolean runtime; private final boolean direct; - private final int audienceSize; + private final int teamSize; private final int documentSize; private List directAttachments = new ArrayList<>(); - public ExecutableInject(boolean runtime, boolean direct, Injection source, Inject inject, Contract contract, List audiences, List users) { + public ExecutableInject(boolean runtime, boolean direct, Injection source, Inject inject, Contract contract, List teams, List users) { this.runtime = runtime; this.direct = direct; this.source = source; this.inject = inject; this.contract = contract; this.users = users; - this.audiences = audiences; - this.audienceSize = audiences.size(); + this.teams = teams; + this.teamSize = teams.size(); this.documentSize = inject.getDocuments().size(); } - public ExecutableInject(boolean runtime, boolean direct, Inject inject, Contract contract, List audiences, List users) { - this(runtime, direct, inject, inject, contract, audiences, users); + public ExecutableInject(boolean runtime, boolean direct, Inject inject, Contract contract, List teams, List users) { + this(runtime, direct, inject, inject, contract, teams, users); } public Inject getInject() { @@ -49,8 +49,8 @@ public Injection getSource() { return source; } - public List getAudiences() { - return audiences; + public List getTeams() { + return teams; } public List getUsers() { @@ -81,7 +81,7 @@ public int getDocumentSize() { return documentSize; } - public int getAudienceSize() { - return audienceSize; + public int getTeamSize() { + return teamSize; } } diff --git a/openex-framework/src/main/java/io/openex/execution/ExecutionContext.java b/openex-framework/src/main/java/io/openex/execution/ExecutionContext.java index 2f0dae1990..04baf08259 100644 --- a/openex-framework/src/main/java/io/openex/execution/ExecutionContext.java +++ b/openex-framework/src/main/java/io/openex/execution/ExecutionContext.java @@ -10,20 +10,20 @@ public class ExecutionContext extends HashMap { - public ExecutionContext(User user, Exercise exercise, List audiences) { + public ExecutionContext(User user, Exercise exercise, List teams) { ProtectUser protectUser = new ProtectUser(user); this.put(USER, protectUser); this.put(EXERCISE, exercise); - this.put(AUDIENCES, audiences); + this.put(TEAMS, teams); } public ProtectUser getUser() { return (ProtectUser) this.get(USER); } - public List getAudiences() { + public List getTeams() { //noinspection unchecked - return (List) this.get(AUDIENCES); + return (List) this.get(TEAMS); } public Exercise getExercise() { diff --git a/openex-framework/src/main/java/io/openex/execution/Injector.java b/openex-framework/src/main/java/io/openex/execution/Injector.java index 646084bc53..2be7dc9e71 100644 --- a/openex-framework/src/main/java/io/openex/execution/Injector.java +++ b/openex-framework/src/main/java/io/openex/execution/Injector.java @@ -51,11 +51,11 @@ public void setFileService(FileService fileService) { public abstract List process(Execution execution, ExecutableInject injection, Contract contract) throws Exception; - private InjectExpectation expectationConverter(Audience audience, ExecutableInject executableInject, Expectation expectation) { + private InjectExpectation expectationConverter(Team team, ExecutableInject executableInject, Expectation expectation) { InjectExpectation expectationExecution = new InjectExpectation(); expectationExecution.setExercise(executableInject.getInject().getExercise()); expectationExecution.setInject(executableInject.getInject()); - expectationExecution.setAudience(audience); + expectationExecution.setTeam(team); expectationExecution.setExpectedScore(expectation.getScore()); expectationExecution.setScore(0); switch (expectation.type()) { @@ -94,11 +94,11 @@ private Execution execute(ExecutableInject executableInject) { // Process the execution List expectations = process(execution, executableInject, contract); // Create the expectations - List audiences = executableInject.getAudiences(); - if (isScheduledInject && !audiences.isEmpty() && !expectations.isEmpty()) { - List executions = audiences.stream() - .flatMap(audience -> expectations.stream() - .map(expectation -> expectationConverter(audience, executableInject, expectation))) + List teams = executableInject.getTeams(); + if (isScheduledInject && !teams.isEmpty() && !expectations.isEmpty()) { + List executions = teams.stream() + .flatMap(team -> expectations.stream() + .map(expectation -> expectationConverter(team, executableInject, expectation))) .toList(); this.injectExpectationRepository.saveAll(executions); } diff --git a/openex-framework/src/main/java/io/openex/service/ExecutionContextService.java b/openex-framework/src/main/java/io/openex/service/ExecutionContextService.java index 8ef7619857..dbfa6891cc 100644 --- a/openex-framework/src/main/java/io/openex/service/ExecutionContextService.java +++ b/openex-framework/src/main/java/io/openex/service/ExecutionContextService.java @@ -28,12 +28,12 @@ public class ExecutionContextService { private final VariableRepository variableRepository; - public ExecutionContext executionContext(@NotNull final User user, Injection injection, String audience) { - return this.executionContext(user, injection, List.of(audience)); + public ExecutionContext executionContext(@NotNull final User user, Injection injection, String team) { + return this.executionContext(user, injection, List.of(team)); } - public ExecutionContext executionContext(@NotNull final User user, Injection injection, List audiences) { - ExecutionContext executionContext = new ExecutionContext(user, injection.getExercise(), audiences); + public ExecutionContext executionContext(@NotNull final User user, Injection injection, List teams) { + ExecutionContext executionContext = new ExecutionContext(user, injection.getExercise(), teams); if (injection.getExercise() != null) { String exerciseId = injection.getExercise().getId(); String queryParams = "?user=" + user.getId() + "&inject=" + injection.getId(); @@ -47,8 +47,8 @@ public ExecutionContext executionContext(@NotNull final User user, Injection inj return executionContext; } - public ExecutionContext executionContext(@NotNull final User user, Exercise exercise, String audience) { - ExecutionContext executionContext = new ExecutionContext(user, exercise, List.of(audience)); + public ExecutionContext executionContext(@NotNull final User user, Exercise exercise, String team) { + ExecutionContext executionContext = new ExecutionContext(user, exercise, List.of(team)); if (exercise != null) { fillDynamicVariable(executionContext, exercise.getId()); } diff --git a/openex-front/src/actions/Audience.js b/openex-front/src/actions/Audience.js deleted file mode 100644 index 7c416b1464..0000000000 --- a/openex-front/src/actions/Audience.js +++ /dev/null @@ -1,47 +0,0 @@ -import * as schema from './Schema'; -import { getReferential, putReferential, postReferential, delReferential } from '../utils/Action'; - -export const fetchAudiences = (exerciseId) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences`; - return getReferential(schema.arrayOfAudiences, uri)(dispatch); -}; - -export const fetchAudiencePlayers = (exerciseId, audienceId) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences/${audienceId}/players`; - return getReferential(schema.arrayOfUsers, uri)(dispatch); -}; - -export const fetchAudience = (exerciseId, audienceId) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences/${audienceId}`; - return getReferential(schema.audience, uri)(dispatch); -}; - -export const updateAudience = (exerciseId, audienceId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences/${audienceId}`; - return putReferential(schema.audience, uri, data)(dispatch); -}; - -export const updateAudienceActivation = (exerciseId, audienceId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences/${audienceId}/activation`; - return putReferential(schema.audience, uri, data)(dispatch); -}; - -export const updateAudiencePlayers = (exerciseId, audienceId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences/${audienceId}/players`; - return putReferential(schema.audience, uri, data)(dispatch); -}; - -export const addAudience = (exerciseId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences`; - return postReferential(schema.audience, uri, data)(dispatch); -}; - -export const deleteAudience = (exerciseId, audienceId) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/audiences/${audienceId}`; - return delReferential(uri, 'audiences', audienceId)(dispatch); -}; - -export const copyAudienceToExercise = (exerciseId, audienceId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/copy-audience/${audienceId}`; - return putReferential(schema.audience, uri, data)(dispatch); -}; diff --git a/openex-front/src/actions/Exercise.js b/openex-front/src/actions/Exercise.js index 6f6333afdc..eb55d6d87f 100644 --- a/openex-front/src/actions/Exercise.js +++ b/openex-front/src/actions/Exercise.js @@ -30,6 +30,35 @@ export const updateExerciseLessons = (exerciseId, data) => (dispatch) => putRefe data, )(dispatch); +export const fetchExerciseTeams = (exerciseId) => (dispatch) => { + const uri = `/api/exercises/${exerciseId}/teams`; + return getReferential(schema.arrayOfTeams, uri)(dispatch); +}; + +export const addExerciseTeams = (exerciseId, data) => (dispatch) => putReferential( + schema.arrayOfTeams, + `/api/exercises/${exerciseId}/teams/add`, + data, +)(dispatch); + +export const removeExerciseTeams = (exerciseId, data) => (dispatch) => putReferential( + schema.arrayOfTeams, + `/api/exercises/${exerciseId}/teams/remove`, + data, +)(dispatch); + +export const addExerciseTeamPlayers = (exerciseId, teamId, data) => (dispatch) => putReferential( + schema.exercise, + `/api/exercises/${exerciseId}/teams/${teamId}/players/add`, + data, +)(dispatch); + +export const removeExerciseTeamPlayers = (exerciseId, teamId, data) => (dispatch) => putReferential( + schema.exercise, + `/api/exercises/${exerciseId}/teams/${teamId}/players/remove`, + data, +)(dispatch); + export const updateExerciseTags = (exerciseId, data) => (dispatch) => putReferential( schema.exercise, `/api/exercises/${exerciseId}/tags`, diff --git a/openex-front/src/actions/Inject.js b/openex-front/src/actions/Inject.js index 52367149d2..3e90188702 100644 --- a/openex-front/src/actions/Inject.js +++ b/openex-front/src/actions/Inject.js @@ -21,9 +21,9 @@ export const fetchExerciseInjects = (exerciseId) => (dispatch) => { return getReferential(schema.arrayOfInjects, uri)(dispatch); }; -export const fetchInjectAudiences = (exerciseId, injectId) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/injects/${injectId}/audiences`; - return getReferential(schema.arrayOfAudiences, uri)(dispatch); +export const fetchInjectTeams = (exerciseId, injectId) => (dispatch) => { + const uri = `/api/exercises/${exerciseId}/injects/${injectId}/teams`; + return getReferential(schema.arrayOfTeams, uri)(dispatch); }; export const updateInject = (exerciseId, injectId, data) => (dispatch) => { @@ -41,8 +41,8 @@ export const updateInjectTrigger = (exerciseId, injectId, data) => (dispatch) => return putReferential(schema.inject, uri, data)(dispatch); }; -export const updateInjectAudiences = (exerciseId, injectId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/injects/${injectId}/audiences`; +export const updateInjectTeams = (exerciseId, injectId, data) => (dispatch) => { + const uri = `/api/exercises/${exerciseId}/injects/${injectId}/teams`; return putReferential(schema.inject, uri, data)(dispatch); }; diff --git a/openex-front/src/actions/Lessons.js b/openex-front/src/actions/Lessons.js index 8eb6f43492..51e9564ef3 100644 --- a/openex-front/src/actions/Lessons.js +++ b/openex-front/src/actions/Lessons.js @@ -90,8 +90,8 @@ export const updateLessonsCategory = (exerciseId, lessonsCategoryId, data) => (d return putReferential(schema.lessonsCategory, uri, data)(dispatch); }; -export const updateLessonsCategoryAudiences = (exerciseId, lessonsCategoryId, data) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/lessons_categories/${lessonsCategoryId}/audiences`; +export const updateLessonsCategoryTeams = (exerciseId, lessonsCategoryId, data) => (dispatch) => { + const uri = `/api/exercises/${exerciseId}/lessons_categories/${lessonsCategoryId}/teams`; return putReferential(schema.lessonsCategory, uri, data)(dispatch); }; diff --git a/openex-front/src/actions/Schema.js b/openex-front/src/actions/Schema.js index cae526a6c5..3b55c917df 100644 --- a/openex-front/src/actions/Schema.js +++ b/openex-front/src/actions/Schema.js @@ -114,12 +114,12 @@ export const dryinject = new schema.Entity( ); export const arrayOfDryinjects = new schema.Array(dryinject); -export const audience = new schema.Entity( - 'audiences', +export const team = new schema.Entity( + 'teams', {}, - { idAttribute: 'audience_id' }, + { idAttribute: 'team_id' }, ); -export const arrayOfAudiences = new schema.Array(audience); +export const arrayOfTeams = new schema.Array(team); export const inject = new schema.Entity( 'injects', @@ -242,7 +242,7 @@ export const storeHelper = (state) => ({ getExercise: (id) => entity(id, 'exercises', state), getExerciseDryruns: (id) => entities('dryruns', state).filter((i) => i.dryrun_exercise === id), getExerciseComchecks: (id) => entities('comchecks', state).filter((i) => i.comcheck_exercise === id), - getExerciseAudiences: (id) => entities('audiences', state).filter((i) => i.audience_exercise === id), + getExerciseTeams: (id) => entities('teams', state).filter((i) => i.team_exercises.includes(id)), getExerciseVariables: (id) => entities('variables', state).filter((i) => i.variable_exercise === id), getExerciseArticles: (id) => entities('articles', state).filter((i) => i.article_exercise === id), getExerciseInjects: (id) => entities('injects', state).filter((i) => i.inject_exercise === id), @@ -250,18 +250,18 @@ export const storeHelper = (state) => ({ (i) => i.communication_exercise === id, ), getExerciseTechnicalInjectsPerType: (id) => { - const typesWithNoAudiences = R.uniq( + const typesWithNoTeams = R.uniq( entities('inject_types', state) .map((t) => ({ type: t.config.type, - hasAudiences: - t.fields.filter((f) => f.name === 'audiences').length > 0, + hasTeams: + t.fields.filter((f) => f.name === 'teams').length > 0, })) - .filter((t) => !t.hasAudiences) + .filter((t) => !t.hasTeams) .map((t) => t.type), ); return R.mergeAll( - typesWithNoAudiences.map((t) => ({ + typesWithNoTeams.map((t) => ({ [t]: entities('injects', state).filter( (i) => i.inject_type === t && i.inject_exercise === id, ), @@ -317,14 +317,14 @@ export const storeHelper = (state) => ({ getInjectTypes: () => entities('inject_types', state), getInjectTypesMap: () => maps('inject_types', state), getInjectTypesMapByType: () => R.indexBy(R.path(['config', 'type']), entities('inject_types', state)), - getInjectTypesWithNoAudiences: () => R.uniq( + getInjectTypesWithNoTeams: () => R.uniq( entities('inject_types', state) .map((t) => ({ - hasAudiences: - t.fields.filter((f) => f.key === 'audiences').length > 0, + hasTeams: + t.fields.filter((f) => f.key === 'teams').length > 0, ...t, })) - .filter((t) => !t.hasAudiences) + .filter((t) => !t.hasTeams) .map((t) => t.config.type), ), getNextInjects: () => { @@ -346,16 +346,16 @@ export const storeHelper = (state) => ({ // documents getDocuments: () => entities('documents', state), getDocumentsMap: () => maps('documents', state), - // audiences - getAudience: (id) => entity(id, 'audiences', state), - getAudienceUsers: (id) => entities('users', state).filter((u) => (entity(id, 'audiences', state) || {}).audience_users?.includes( + // teams + getTeam: (id) => entity(id, 'teams', state), + getTeamUsers: (id) => entities('users', state).filter((u) => (entity(id, 'teams', state) || {}).team_users?.includes( u.user_id, )), - getAudienceInjects: (id) => entities('injects', state).filter((i) => (entity(id, 'audiences', state) || {}).audience_injects?.includes( + getTeamInjects: (id) => entities('injects', state).filter((i) => (entity(id, 'teams', state) || {}).team_injects?.includes( i.inject_id, )), - getAudiences: () => entities('audiences', state), - getAudiencesMap: () => maps('audiences', state), + getTeams: () => entities('teams', state), + getTeamsMap: () => maps('teams', state), getSettings: () => { return R.mergeAll( Object.entries(state.referential.entities.parameters ?? {}).map( diff --git a/openex-front/src/actions/Team.js b/openex-front/src/actions/Team.js new file mode 100644 index 0000000000..813fe89013 --- /dev/null +++ b/openex-front/src/actions/Team.js @@ -0,0 +1,37 @@ +import * as schema from './Schema'; +import { getReferential, putReferential, postReferential, delReferential } from '../utils/Action'; + +export const fetchTeams = () => (dispatch) => { + const uri = '/api/teams'; + return getReferential(schema.arrayOfTeams, uri)(dispatch); +}; + +export const fetchTeamPlayers = (teamId) => (dispatch) => { + const uri = `/api/teams/${teamId}/players`; + return getReferential(schema.arrayOfUsers, uri)(dispatch); +}; + +export const fetchTeam = (exerciseId, teamId) => (dispatch) => { + const uri = `/api/exercises/${exerciseId}/teams/${teamId}`; + return getReferential(schema.team, uri)(dispatch); +}; + +export const updateTeam = (teamId, data) => (dispatch) => { + const uri = `/api/teams/${teamId}`; + return putReferential(schema.team, uri, data)(dispatch); +}; + +export const updateTeamPlayers = (teamId, data) => (dispatch) => { + const uri = `/api/teams/${teamId}/players`; + return putReferential(schema.team, uri, data)(dispatch); +}; + +export const addTeam = (data) => (dispatch) => { + const uri = '/api/teams'; + return postReferential(schema.team, uri, data)(dispatch); +}; + +export const deleteTeam = (teamId) => (dispatch) => { + const uri = `/api/teams/${teamId}`; + return delReferential(uri, 'teams', teamId)(dispatch); +}; diff --git a/openex-front/src/actions/helper.d.ts b/openex-front/src/actions/helper.d.ts index 3418f15e65..29b1f2c481 100644 --- a/openex-front/src/actions/helper.d.ts +++ b/openex-front/src/actions/helper.d.ts @@ -1,4 +1,4 @@ -import type { Audience, Exercise, InjectExpectation, Media, Organization, Tag, User } from '../utils/api-types'; +import type { Team, Exercise, InjectExpectation, Media, Organization, Tag, User } from '../utils/api-types'; export interface ExercicesHelper { getExercise: (exerciseId: Exercise['exercise_id']) => Exercise; @@ -17,9 +17,9 @@ export interface TagsHelper { getTagsMap: () => Record; } -export interface AudiencesHelper { - getExerciseAudiences: (exerciseId: Exercise['exercise_id']) => Audience[]; - getAudiencesMap: () => Record; +export interface TeamsHelper { + getExerciseTeams: (exerciseId: Exercise['exercise_id']) => Team[]; + getTeamsMap: () => Record; } export interface MediasHelper { diff --git a/openex-front/src/admin/Index.tsx b/openex-front/src/admin/Index.tsx index fce15efd90..a77e41ae3e 100644 --- a/openex-front/src/admin/Index.tsx +++ b/openex-front/src/admin/Index.tsx @@ -16,7 +16,7 @@ const IndexExercise = lazy(() => import('./components/exercises/Index')); const Dashboard = lazy(() => import('./components/Dashboard')); const IndexProfile = lazy(() => import('./components/profile/Index')); const Exercises = lazy(() => import('./components/exercises/Exercises')); -const Players = lazy(() => import('./components/players/Players')); +const Persons = lazy(() => import('./components/persons/Index')); const Organizations = lazy(() => import('./components/organizations/Organizations')); const Documents = lazy(() => import('./components/documents/Documents')); const Medias = lazy(() => import('./components/medias/Medias')); @@ -69,7 +69,7 @@ const Index = () => { - + diff --git a/openex-front/src/admin/components/exercises/DefinitionMenu.tsx b/openex-front/src/admin/components/exercises/DefinitionMenu.tsx index e94bdca26d..2d733ed50a 100644 --- a/openex-front/src/admin/components/exercises/DefinitionMenu.tsx +++ b/openex-front/src/admin/components/exercises/DefinitionMenu.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Link, useLocation } from 'react-router-dom'; import { Drawer, MenuList, MenuItem, ListItemIcon, ListItemText } from '@mui/material'; -import { AttachMoneyOutlined, CastForEducationOutlined, EmojiEventsOutlined } from '@mui/icons-material'; +import { AttachMoneyOutlined, GroupsOutlined, EmojiEventsOutlined } from '@mui/icons-material'; import { NewspaperVariantMultipleOutline } from 'mdi-material-ui'; import { makeStyles } from '@mui/styles'; import { useFormatter } from '../../../components/i18n'; @@ -42,17 +42,17 @@ const DefinitionMenu: React.FC = ({ exerciseId }) => { - + - + { const [openComcheckDelete, setOpenComcheckDelete] = useState(null); const [openDryrunDelete, setOpenDryrunDelete] = useState(null); const { t, nsd, fldt } = useFormatter(); - const { exercise, audiences, dryruns, comchecks } = useHelper((helper) => { + const { exercise, teams, dryruns, comchecks } = useHelper((helper) => { const ex = helper.getExercise(exerciseId); - const aud = helper.getExerciseAudiences(exerciseId); + const aud = helper.getExerciseTeams(exerciseId); const dry = helper.getExerciseDryruns(exerciseId); const com = helper.getExerciseComchecks(exerciseId); - return { exercise: ex, audiences: aud, dryruns: dry, comchecks: com }; + return { exercise: ex, teams: aud, dryruns: dry, comchecks: com }; }); useDataLoader(() => { - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); dispatch(fetchComchecks(exerciseId)); dispatch(fetchDryruns(exerciseId)); }); @@ -197,29 +196,29 @@ const Exercise = () => { ]), )(exercise); const mapIndexed = R.addIndex(R.map); - const audiencesColors = R.pipe( + const teamsColors = R.pipe( mapIndexed((a, index) => [ - a.audience_id, + a.team_id, colors(theme.palette.mode === 'dark' ? 400 : 600)[index], ]), R.fromPairs, - )(audiences); - const topAudiences = R.pipe( - R.sortWith([R.descend(R.prop('audience_injects_number'))]), + )(teams); + const topTeams = R.pipe( + R.sortWith([R.descend(R.prop('team_injects_number'))]), R.take(6), - )(audiences || []); + )(teams || []); const distributionChartData = [ { name: t('Number of injects'), - data: topAudiences.map((a) => ({ - x: a.audience_name, - y: a.audience_injects_number, - fillColor: audiencesColors[a.audience_id], + data: topTeams.map((a) => ({ + x: a.team_name, + y: a.team_injects_number, + fillColor: teamsColors[a.team_id], })), }, ]; const maxInjectsNumber = Math.max( - ...topAudiences.map((a) => a.audience_injects_number), + ...topTeams.map((a) => a.team_injects_number), ); const nextInjectDate = exercise.exercise_next_inject_date ? new Date(exercise.exercise_next_inject_date).getTime() @@ -442,7 +441,7 @@ const Exercise = () => { {t('Injects distribution')} - {topAudiences.length > 0 ? ( + {topTeams.length > 0 ? ( { series={distributionChartData} type="bar" width="100%" - height={50 + topAudiences.length * 50} + height={50 + topTeams.length * 50} /> ) : ( - + )} diff --git a/openex-front/src/admin/components/exercises/ExercisePopover.tsx b/openex-front/src/admin/components/exercises/ExercisePopover.tsx index 43033b2c5d..98a573935a 100644 --- a/openex-front/src/admin/components/exercises/ExercisePopover.tsx +++ b/openex-front/src/admin/components/exercises/ExercisePopover.tsx @@ -211,7 +211,7 @@ const ExercisePopover: FunctionComponent = ({ - {t('Audiences')} + {t('Teams')} diff --git a/openex-front/src/admin/components/exercises/Index.tsx b/openex-front/src/admin/components/exercises/Index.tsx index f2810d85d7..0108a08b35 100644 --- a/openex-front/src/admin/components/exercises/Index.tsx +++ b/openex-front/src/admin/components/exercises/Index.tsx @@ -19,7 +19,7 @@ const Dashboard = lazy(() => import('./dashboard/Dashboard')); const Lessons = lazy(() => import('./lessons/Lessons')); const Reports = lazy(() => import('./reports/Reports')); const Report = lazy(() => import('./reports/Report')); -const Audiences = lazy(() => import('./audiences/Audiences')); +const Teams = lazy(() => import('./teams/Teams')); const Injects = lazy(() => import('./injects/Injects')); const Articles = lazy(() => import('./articles/Articles')); const Challenges = lazy(() => import('./challenges/Challenges')); @@ -63,7 +63,7 @@ const Index = () => { - + diff --git a/openex-front/src/admin/components/exercises/audiences/AudienceForm.js b/openex-front/src/admin/components/exercises/audiences/AudienceForm.js deleted file mode 100644 index 12b9a027b5..0000000000 --- a/openex-front/src/admin/components/exercises/audiences/AudienceForm.js +++ /dev/null @@ -1,90 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { Form } from 'react-final-form'; -import { Button } from '@mui/material'; -import TextField from '../../../../components/TextField'; -import inject18n from '../../../../components/i18n'; -import TagField from '../../../../components/TagField'; - -class AudienceForm extends Component { - validate(values) { - const { t } = this.props; - const errors = {}; - const requiredFields = ['audience_name']; - requiredFields.forEach((field) => { - if (!values[field]) { - errors[field] = t('This field is required.'); - } - }); - return errors; - } - - render() { - const { t, onSubmit, handleClose, initialValues, editing } = this.props; - return ( -
{ - changeValue(state, field, () => value); - }, - }} - > - {({ handleSubmit, form, values, submitting, pristine }) => ( - - - - -
- - -
- - )} - - ); - } -} - -AudienceForm.propTypes = { - t: PropTypes.func, - onSubmit: PropTypes.func.isRequired, - handleClose: PropTypes.func, - editing: PropTypes.bool, -}; - -export default inject18n(AudienceForm); diff --git a/openex-front/src/admin/components/exercises/audiences/AudiencePopover.js b/openex-front/src/admin/components/exercises/audiences/AudiencePopover.js deleted file mode 100644 index c695dea9b4..0000000000 --- a/openex-front/src/admin/components/exercises/audiences/AudiencePopover.js +++ /dev/null @@ -1,341 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import * as R from 'ramda'; -import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, IconButton, Menu, MenuItem } from '@mui/material'; -import { MoreVert } from '@mui/icons-material'; -import { updateAudience, deleteAudience, updateAudienceActivation } from '../../../../actions/Audience'; -import inject18n from '../../../../components/i18n'; -import AudienceForm from './AudienceForm'; -import { isExerciseReadOnly } from '../../../../utils/Exercise'; -import { Transition } from '../../../../utils/Environment'; -import { storeHelper } from '../../../../actions/Schema'; -import { tagOptions } from '../../../../utils/Option'; - -class AudiencePopover extends Component { - constructor(props) { - super(props); - this.state = { - openDelete: false, - openEdit: false, - openRemove: false, - openEnable: false, - openDisable: false, - openPopover: false, - }; - } - - handlePopoverOpen(event) { - event.stopPropagation(); - this.setState({ anchorEl: event.currentTarget }); - } - - handlePopoverClose() { - this.setState({ anchorEl: null }); - } - - handleOpenEdit() { - this.setState({ openEdit: true }); - this.handlePopoverClose(); - } - - handleCloseEdit() { - this.setState({ openEdit: false }); - } - - onSubmitEdit(data) { - const inputValues = R.pipe( - R.assoc('audience_tags', R.pluck('id', data.audience_tags)), - )(data); - return this.props - .updateAudience( - this.props.exerciseId, - this.props.audience.audience_id, - inputValues, - ) - .then(() => this.handleCloseEdit()); - } - - handleOpenDelete() { - this.setState({ openDelete: true }); - this.handlePopoverClose(); - } - - handleCloseDelete() { - this.setState({ openDelete: false }); - } - - submitDelete() { - this.props.deleteAudience( - this.props.exerciseId, - this.props.audience.audience_id, - ); - this.handleCloseDelete(); - } - - handleOpenRemove() { - this.setState({ openRemove: true }); - this.handlePopoverClose(); - } - - handleCloseRemove() { - this.setState({ openRemove: false }); - } - - submitRemove() { - this.props.onRemoveAudience(this.props.audience.audience_id); - this.handleCloseRemove(); - } - - handleOpenEnable() { - this.setState({ - openEnable: true, - }); - this.handlePopoverClose(); - } - - handleCloseEnable() { - this.setState({ - openEnable: false, - }); - } - - submitEnable() { - this.props.updateAudienceActivation( - this.props.exerciseId, - this.props.audience.audience_id, - { audience_enabled: true }, - ); - this.handleCloseEnable(); - } - - handleOpenDisable() { - this.setState({ - openDisable: true, - }); - this.handlePopoverClose(); - } - - handleCloseDisable() { - this.setState({ - openDisable: false, - }); - } - - submitDisable() { - this.props.updateAudienceActivation( - this.props.exerciseId, - this.props.audience.audience_id, - { audience_enabled: false }, - ); - this.handleCloseDisable(); - } - - handleOpenEditPlayers() { - this.props.setSelectedAudience(this.props.audience.audience_id); - this.handlePopoverClose(); - } - - render() { - const { - t, - audience, - setSelectedAudience, - exercise, - onRemoveAudience, - tagsMap, - disabled, - } = this.props; - const audienceTags = tagOptions(audience.audience_tags, tagsMap); - const initialValues = R.pipe( - R.assoc('audience_tags', audienceTags), - R.pick(['audience_name', 'audience_description', 'audience_tags']), - )(audience); - return ( -
- - - - - - {t('Update')} - - {setSelectedAudience && ( - - {t('Manage players')} - - )} - {audience.audience_enabled ? ( - - {t('Disable')} - - ) : ( - - {t('Enable')} - - )} - {onRemoveAudience && ( - - {t('Remove from the inject')} - - )} - {!onRemoveAudience && ( - - {t('Delete')} - - )} - - - - - {t('Do you want to delete this audience?')} - - - - - - - - - {t('Update the audience')} - - - - - - - - {t('Do you want to remove the audience from the inject?')} - - - - - - - - - - - {t('Do you want to enable this audience?')} - - - - - - - - - - - {t('Do you want to disable this audience?')} - - - - - - - -
- ); - } -} - -AudiencePopover.propTypes = { - t: PropTypes.func, - exerciseId: PropTypes.string, - exercise: PropTypes.object, - audience: PropTypes.object, - updateAudience: PropTypes.func, - updateAudienceActivation: PropTypes.func, - deleteAudience: PropTypes.func, - setSelectedAudience: PropTypes.func, - onRemoveAudience: PropTypes.func, - disabled: PropTypes.bool, -}; - -const select = (state) => { - const helper = storeHelper(state); - const tagsMap = helper.getTagsMap(); - return { tagsMap }; -}; - -export default R.compose( - connect(select, { - updateAudience, - deleteAudience, - updateAudienceActivation, - }), - inject18n, -)(AudiencePopover); diff --git a/openex-front/src/admin/components/exercises/audiences/CreateAudience.js b/openex-front/src/admin/components/exercises/audiences/CreateAudience.js deleted file mode 100644 index 61fe66afa1..0000000000 --- a/openex-front/src/admin/components/exercises/audiences/CreateAudience.js +++ /dev/null @@ -1,125 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import * as R from 'ramda'; -import withStyles from '@mui/styles/withStyles'; -import { Fab, Dialog, DialogTitle, DialogContent, Slide, ListItem, ListItemIcon, ListItemText } from '@mui/material'; -import { Add, ControlPointOutlined } from '@mui/icons-material'; -import AudienceForm from './AudienceForm'; -import { addAudience } from '../../../../actions/Audience'; -import inject18n from '../../../../components/i18n'; - -const Transition = React.forwardRef((props, ref) => ( - -)); -Transition.displayName = 'TransitionSlide'; - -const styles = (theme) => ({ - createButton: { - position: 'fixed', - bottom: 30, - right: 230, - }, - text: { - fontSize: 15, - color: theme.palette.primary.main, - fontWeight: 500, - }, -}); - -class CreateAudience extends Component { - constructor(props) { - super(props); - this.state = { open: false }; - } - - handleOpen() { - this.setState({ open: true }); - } - - handleClose() { - this.setState({ open: false }); - } - - onSubmit(data) { - const inputValues = R.pipe( - R.assoc('audience_tags', R.pluck('id', data.audience_tags)), - )(data); - return this.props - .addAudience(this.props.exerciseId, inputValues) - .then((result) => { - if (result.result) { - if (this.props.onCreate) { - this.props.onCreate(result.result); - } - return this.handleClose(); - } - return result; - }); - } - - render() { - const { classes, t, inline } = this.props; - return ( -
- {inline === true ? ( - - - - - - - ) : ( - - - - )} - - {t('Create a new audience')} - - - - -
- ); - } -} - -CreateAudience.propTypes = { - exerciseId: PropTypes.string, - classes: PropTypes.object, - t: PropTypes.func, - addAudience: PropTypes.func, - inline: PropTypes.bool, - onCreate: PropTypes.func, -}; - -export default R.compose( - connect(null, { addAudience }), - inject18n, - withStyles(styles), -)(CreateAudience); diff --git a/openex-front/src/admin/components/exercises/controls/ComcheckForm.js b/openex-front/src/admin/components/exercises/controls/ComcheckForm.js index d6f29fdf51..3e75431d51 100644 --- a/openex-front/src/admin/components/exercises/controls/ComcheckForm.js +++ b/openex-front/src/admin/components/exercises/controls/ComcheckForm.js @@ -15,7 +15,7 @@ class ComcheckForm extends Component { const errors = {}; const requiredFields = [ 'comcheck_name', - 'comcheck_audiences', + 'comcheck_teams', 'comcheck_end_date', 'comcheck_subject', 'comcheck_message', @@ -29,8 +29,8 @@ class ComcheckForm extends Component { } render() { - const { t, onSubmit, handleClose, initialValues, audiences } = this.props; - const audiencesbyId = R.indexBy(R.prop('audience_id'), audiences); + const { t, onSubmit, handleClose, initialValues, teams } = this.props; + const teamsbyId = R.indexBy(R.prop('team_id'), teams); return (
@@ -132,7 +132,7 @@ ComcheckForm.propTypes = { onSubmit: PropTypes.func.isRequired, handleClose: PropTypes.func, editing: PropTypes.bool, - audiences: PropTypes.array, + teams: PropTypes.array, }; export default inject18n(ComcheckForm); diff --git a/openex-front/src/admin/components/exercises/controls/CreateControl.tsx b/openex-front/src/admin/components/exercises/controls/CreateControl.tsx index 4eaa82d49a..a4c3ad9564 100644 --- a/openex-front/src/admin/components/exercises/controls/CreateControl.tsx +++ b/openex-front/src/admin/components/exercises/controls/CreateControl.tsx @@ -16,7 +16,7 @@ import { addDryrun } from '../../../../actions/Dryrun'; import { useFormatter } from '../../../../components/i18n'; import Transition from '../../../../components/common/Transition'; import { useHelper } from '../../../../store'; -import type { AudiencesHelper, ExercicesHelper, UsersHelper } from '../../../../actions/helper'; +import type { TeamsHelper, ExercicesHelper, UsersHelper } from '../../../../actions/helper'; const useStyles = makeStyles(() => ({ createButton: { @@ -40,12 +40,12 @@ const CreateControl: React.FC = ({ exerciseId, variant }) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const { me, exercise, audiences } = useHelper( - (helper: UsersHelper & ExercicesHelper & AudiencesHelper) => { + const { me, exercise, teams } = useHelper( + (helper: UsersHelper & ExercicesHelper & TeamsHelper) => { return { me: helper.getMe(), exercise: helper.getExercise(exerciseId), - audiences: helper.getExerciseAudiences(exerciseId), + teams: helper.getExerciseTeams(exerciseId), }; }, ); @@ -134,7 +134,7 @@ const CreateControl: React.FC = ({ exerciseId, variant }) => {
${t( 'This is a communication check before the beginning of the exercise. Please click on the following link' @@ -143,7 +143,7 @@ const CreateControl: React.FC = ({ exerciseId, variant }) => { 'The exercise control team', )}`, }} - audiences={audiences} + teams={teams} handleClose={() => setOpenComcheck(false)} /> diff --git a/openex-front/src/admin/components/exercises/dashboard/Dashboard.js b/openex-front/src/admin/components/exercises/dashboard/Dashboard.js index afbb907eca..edbceced33 100644 --- a/openex-front/src/admin/components/exercises/dashboard/Dashboard.js +++ b/openex-front/src/admin/components/exercises/dashboard/Dashboard.js @@ -7,11 +7,10 @@ import { useDispatch } from 'react-redux'; import { useFormatter } from '../../../../components/i18n'; import { useHelper } from '../../../../store'; import useDataLoader from '../../../../utils/ServerSideEvent'; -import { fetchAudiences } from '../../../../actions/Audience'; import ResultsMenu from '../ResultsMenu'; import { fetchInjects, fetchInjectTypes } from '../../../../actions/Inject'; import { fetchExerciseChallenges } from '../../../../actions/Challenge'; -import { fetchExerciseInjectExpectations } from '../../../../actions/Exercise'; +import { fetchExerciseInjectExpectations, fetchExerciseTeams } from '../../../../actions/Exercise'; import { fetchPlayers } from '../../../../actions/User'; import { fetchOrganizations } from '../../../../actions/Organization'; import { fetchExerciseCommunications } from '../../../../actions/Communication'; @@ -57,11 +56,11 @@ const Dashboard = () => { const { exerciseId } = useParams(); const { exercise, - audiences, + teams, injects, challengesMap, injectTypesMap, - audiencesMap, + teamsMap, injectExpectations, injectsMap, usersMap, @@ -71,8 +70,8 @@ const Dashboard = () => { } = useHelper((helper) => { return { exercise: helper.getExercise(exerciseId), - audiences: helper.getExerciseAudiences(exerciseId), - audiencesMap: helper.getAudiencesMap(), + teams: helper.getExerciseTeams(exerciseId), + teamsMap: helper.getTeamsMap(), injects: helper.getExerciseInjects(exerciseId), injectsMap: helper.getInjectsMap(), usersMap: helper.getUsersMap(), @@ -85,7 +84,7 @@ const Dashboard = () => { }; }); useDataLoader(() => { - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); dispatch(fetchInjectTypes()); dispatch(fetchInjects(exerciseId)); dispatch(fetchExerciseChallenges(exerciseId)); @@ -125,8 +124,8 @@ const Dashboard = () => {
-
{t('Audiences')}
-
{(audiences || []).length}
+
{t('Teams')}
+
{(teams || []).length}
@@ -146,12 +145,12 @@ const Dashboard = () => { {t('Exercise definition and scenario')} { {t('Exercise data')} { ({ const DashboardDataStatistics = ({ injectsMap, - audiences, + teams, injects, communications, usersMap, @@ -29,13 +29,13 @@ const DashboardDataStatistics = ({ const theme = useTheme(); let cumulation = 0; const mapIndexed = R.addIndex(R.map); - const audiencesColors = R.pipe( + const teamsColors = R.pipe( mapIndexed((a, index) => [ - a.audience_id, + a.team_id, colors(theme.palette.mode === 'dark' ? 400 : 600)[index], ]), R.fromPairs, - )(audiences); + )(teams); const injectsOverTime = R.pipe( R.filter((i) => i && i.inject_sent_at !== null), R.sortWith([R.ascend(R.prop('inject_sent_at'))]), @@ -53,11 +53,11 @@ const DashboardDataStatistics = ({ })), }, ]; - const audiencesInjects = R.pipe( + const teamsInjects = R.pipe( R.map((n) => { cumulation = 0; return R.assoc( - 'audience_injects', + 'team_injects', R.pipe( R.map((i) => injectsMap[i]), R.filter((i) => i && i.inject_sent_at !== null), @@ -66,19 +66,19 @@ const DashboardDataStatistics = ({ cumulation += 1; return R.assoc('inject_cumulated_number', cumulation, i); }), - )(n.audience_injects), + )(n.team_injects), n, ); }), R.map((a) => ({ - name: a.audience_name, - color: audiencesColors[a.audience_id], - data: a.audience_injects.map((i) => ({ + name: a.team_name, + color: teamsColors[a.team_id], + data: a.team_injects.map((i) => ({ x: i.inject_sent_at, y: i.inject_cumulated_number, })), })), - )(audiences); + )(teams); const communicationsOverTime = R.pipe( R.sortWith([R.ascend(R.prop('communication_received_at'))]), R.map((i) => { @@ -95,46 +95,46 @@ const DashboardDataStatistics = ({ })), }, ]; - const audiencesCommunications = R.pipe( + const teamsCommunications = R.pipe( R.map((n) => { cumulation = 0; return R.assoc( - 'audience_communications', + 'team_communications', R.pipe( R.sortWith([R.ascend(R.prop('communication_received_at'))]), R.map((i) => { cumulation += 1; return R.assoc('communication_cumulated_number', cumulation, i); }), - )(n.audience_communications), + )(n.team_communications), n, ); }), R.map((a) => ({ - name: a.audience_name, - color: audiencesColors[a.audience_id], - data: a.audience_communications.map((c) => ({ + name: a.team_name, + color: teamsColors[a.team_id], + data: a.team_communications.map((c) => ({ x: c.communication_received_at, y: c.communication_cumulated_number, })), })), - )(audiences); - const sortedAudiencesByCommunicationNumber = R.pipe( + )(teams); + const sortedTeamsByCommunicationNumber = R.pipe( R.map((a) => R.assoc( - 'audience_communications_number', - a.audience_communications.length, + 'team_communications_number', + a.team_communications.length, a, )), - R.sortWith([R.descend(R.prop('audience_communications_number'))]), + R.sortWith([R.descend(R.prop('team_communications_number'))]), R.take(10), - )(audiences || []); - const totalMailsByAudienceData = [ + )(teams || []); + const totalMailsByTeamData = [ { name: t('Total mails'), - data: sortedAudiencesByCommunicationNumber.map((a) => ({ - x: a.audience_name, - y: a.audience_communications_number, - fillColor: audiencesColors[a.audience_id], + data: sortedTeamsByCommunicationNumber.map((a) => ({ + x: a.team_name, + y: a.team_communications_number, + fillColor: teamsColors[a.team_id], })), }, ]; @@ -205,7 +205,7 @@ const DashboardDataStatistics = ({ {t('Sent injects over time')} - {audiencesInjects.length > 0 ? ( + {teamsInjects.length > 0 ? ( {t('Sent mails over time')} - {audiencesCommunications.length > 0 ? ( + {teamsCommunications.length > 0 ? ( - {t('Distribution of mails by audience')} + {t('Distribution of mails by team')} - {sortedAudiencesByCommunicationNumber.length > 0 ? ( + {sortedTeamsByCommunicationNumber.length > 0 ? ( ) : ( ({ })); const DashboardDefinitionScoreStatistics = ({ - audiences, + teams, injects, injectTypesMap, challengesMap, @@ -26,13 +26,13 @@ const DashboardDefinitionScoreStatistics = ({ const { t, tPick } = useFormatter(); const theme = useTheme(); const mapIndexed = R.addIndex(R.map); - const audiencesColors = R.pipe( + const teamsColors = R.pipe( mapIndexed((a, index) => [ - a.audience_id, + a.team_id, colors(theme.palette.mode === 'dark' ? 400 : 600)[index], ]), R.fromPairs, - )(audiences); + )(teams); const injectTypesWithScore = R.pipe( R.filter( (n) => n.inject_type === 'openex_challenge' @@ -90,33 +90,33 @@ const DashboardDefinitionScoreStatistics = ({ })), }, ]; - const sortedAudiencesByExpectation = R.pipe( - R.sortWith([R.descend(R.prop('audience_injects_expectations_number'))]), + const sortedTeamsByExpectation = R.pipe( + R.sortWith([R.descend(R.prop('team_injects_expectations_number'))]), R.take(10), - )(audiences || []); - const expectationsByAudienceData = [ + )(teams || []); + const expectationsByTeamData = [ { name: t('Number of expectations'), - data: sortedAudiencesByExpectation.map((a) => ({ - x: a.audience_name, - y: a.audience_injects_expectations_number, - fillColor: audiencesColors[a.audience_id], + data: sortedTeamsByExpectation.map((a) => ({ + x: a.team_name, + y: a.team_injects_expectations_number, + fillColor: teamsColors[a.team_id], })), }, ]; - const sortedAudiencesByExpectedScore = R.pipe( + const sortedTeamsByExpectedScore = R.pipe( R.sortWith([ - R.descend(R.prop('audience_injects_expectations_total_expected_score')), + R.descend(R.prop('team_injects_expectations_total_expected_score')), ]), R.take(10), - )(audiences || []); - const expectedScoreByAudienceData = [ + )(teams || []); + const expectedScoreByTeamData = [ { name: t('Total expected score'), - data: sortedAudiencesByExpectedScore.map((a) => ({ - x: a.audience_name, - y: a.audience_injects_expectations_total_expected_score, - fillColor: audiencesColors[a.audience_id], + data: sortedTeamsByExpectedScore.map((a) => ({ + x: a.team_name, + y: a.team_injects_expectations_total_expected_score, + fillColor: teamsColors[a.team_id], })), }, ]; @@ -168,37 +168,37 @@ const DashboardDefinitionScoreStatistics = ({ - {t('Distribution of expectations by audience')} + {t('Distribution of expectations by team')} - {sortedAudiencesByExpectation.length > 0 ? ( + {sortedTeamsByExpectation.length > 0 ? ( ) : ( - + )} - {t('Distribution of expected total score by audience')} + {t('Distribution of expected total score by team')} - {sortedAudiencesByExpectedScore.length > 0 ? ( + {sortedTeamsByExpectedScore.length > 0 ? ( ) : ( - + )} diff --git a/openex-front/src/admin/components/exercises/dashboard/DashboardDefinitionStatistics.js b/openex-front/src/admin/components/exercises/dashboard/DashboardDefinitionStatistics.js index d86b94945d..71a53e60ff 100644 --- a/openex-front/src/admin/components/exercises/dashboard/DashboardDefinitionStatistics.js +++ b/openex-front/src/admin/components/exercises/dashboard/DashboardDefinitionStatistics.js @@ -17,7 +17,7 @@ const useStyles = makeStyles(() => ({ })); const DashboardDefinitionStatistics = ({ - audiences, + teams, injects, injectTypesMap, }) => { @@ -25,24 +25,24 @@ const DashboardDefinitionStatistics = ({ const { t, tPick } = useFormatter(); const theme = useTheme(); const mapIndexed = R.addIndex(R.map); - const audiencesColors = R.pipe( + const teamsColors = R.pipe( mapIndexed((a, index) => [ - a.audience_id, + a.team_id, colors(theme.palette.mode === 'dark' ? 400 : 600)[index], ]), R.fromPairs, - )(audiences); - const sortedAudiencesByInjectsNumber = R.pipe( - R.sortWith([R.descend(R.prop('audience_injects_number'))]), + )(teams); + const sortedTeamsByInjectsNumber = R.pipe( + R.sortWith([R.descend(R.prop('team_injects_number'))]), R.take(10), - )(audiences || []); - const injectsByAudienceData = [ + )(teams || []); + const injectsByTeamData = [ { name: t('Number of injects'), - data: sortedAudiencesByInjectsNumber.map((a) => ({ - x: a.audience_name, - y: a.audience_injects_number, - fillColor: audiencesColors[a.audience_id], + data: sortedTeamsByInjectsNumber.map((a) => ({ + x: a.team_name, + y: a.team_injects_number, + fillColor: teamsColors[a.team_id], })), }, ]; @@ -93,16 +93,16 @@ const DashboardDefinitionStatistics = ({ - {t('Distribution of injects by audience')} + {t('Distribution of injects by team')} - {sortedAudiencesByInjectsNumber.length > 0 ? ( + {sortedTeamsByInjectsNumber.length > 0 ? ( ) : ( ({ })); const DashboardDefinitionStatistics = ({ - audiences, + teams, organizations, - audiencesMap, + teamsMap, injectExpectations, injectsMap, injectTypesMap, @@ -31,13 +31,13 @@ const DashboardDefinitionStatistics = ({ const { t, nsdt, tPick } = useFormatter(); const theme = useTheme(); const mapIndexed = R.addIndex(R.map); - const audiencesColors = R.pipe( + const teamsColors = R.pipe( mapIndexed((a, index) => [ - a.audience_id, + a.team_id, colors(theme.palette.mode === 'dark' ? 400 : 600)[index], ]), R.fromPairs, - )(audiences); + )(teams); const organizationsColors = R.pipe( mapIndexed((o, index) => [ o.organization_id, @@ -46,9 +46,9 @@ const DashboardDefinitionStatistics = ({ R.fromPairs, )(organizations); let cumulation = 0; - const audiencesScores = R.pipe( + const teamsScores = R.pipe( R.filter((n) => n.inject_expectation_result !== null), - R.groupBy(R.prop('inject_expectation_audience')), + R.groupBy(R.prop('inject_expectation_team')), R.toPairs, R.map((n) => { cumulation = 0; @@ -64,17 +64,17 @@ const DashboardDefinitionStatistics = ({ ]; }), R.map((n) => ({ - name: audiencesMap[n[0]]?.audience_name, - color: audiencesColors[n[0]], + name: teamsMap[n[0]]?.team_name, + color: teamsColors[n[0]], data: n[1].map((i) => ({ x: i.inject_expectation_updated_at, y: i.inject_expectation_cumulated_score, })), })), )(injectExpectations); - const audiencesPercentScoresData = R.pipe( + const teamsPercentScoresData = R.pipe( R.filter((n) => n.inject_expectation_result !== null), - R.groupBy(R.prop('inject_expectation_audience')), + R.groupBy(R.prop('inject_expectation_team')), R.toPairs, R.map((n) => { cumulation = 0; @@ -88,9 +88,9 @@ const DashboardDefinitionStatistics = ({ 'inject_expectation_percent_score', Math.round( (cumulation * 100) - / (audiencesMap[n[0]] - ? audiencesMap[n[0]] - .audience_injects_expectations_total_expected_score + / (teamsMap[n[0]] + ? teamsMap[n[0]] + .team_injects_expectations_total_expected_score : 1), ), i, @@ -100,8 +100,8 @@ const DashboardDefinitionStatistics = ({ ]; }), R.map((n) => ({ - name: audiencesMap[n[0]]?.audience_name, - color: audiencesColors[n[0]], + name: teamsMap[n[0]]?.team_name, + color: teamsColors[n[0]], data: n[1].map((i) => ({ x: i.inject_expectation_updated_at, y: i.inject_expectation_percent_score, @@ -188,28 +188,28 @@ const DashboardDefinitionStatistics = ({ })), }, ]; - const audiencesTotalScores = R.pipe( + const teamsTotalScores = R.pipe( R.filter((n) => n.inject_expectation_result !== null), - R.groupBy(R.prop('inject_expectation_audience')), + R.groupBy(R.prop('inject_expectation_team')), R.toPairs, R.map((n) => ({ - ...audiencesMap[n[0]], - audience_total_score: R.sum( + ...teamsMap[n[0]], + team_total_score: R.sum( R.map((o) => o.inject_expectation_score, n[1]), ), })), )(injectExpectations); - const sortedAudiencesByTotalScore = R.pipe( - R.sortWith([R.descend(R.prop('audience_total_score'))]), + const sortedTeamsByTotalScore = R.pipe( + R.sortWith([R.descend(R.prop('team_total_score'))]), R.take(10), - )(audiencesTotalScores); - const totalScoreByAudienceData = [ + )(teamsTotalScores); + const totalScoreByTeamData = [ { name: t('Total score'), - data: sortedAudiencesByTotalScore.map((a) => ({ - x: a.audience_name, - y: a.audience_total_score, - fillColor: audiencesColors[a.audience_id], + data: sortedTeamsByTotalScore.map((a) => ({ + x: a.team_name, + y: a.team_total_score, + fillColor: teamsColors[a.team_id], })), }, ]; @@ -271,28 +271,28 @@ const DashboardDefinitionStatistics = ({ })), }, ]; - const audiencesByPercentScore = R.map( + const teamsByPercentScore = R.map( (n) => R.assoc( - 'audience_total_percent_score', + 'team_total_percent_score', Math.round( - (n.audience_injects_expectations_total_score * 100) - / n.audience_injects_expectations_total_expected_score, + (n.team_injects_expectations_total_score * 100) + / n.team_injects_expectations_total_expected_score, ), n, ), - audiencesTotalScores, + teamsTotalScores, ); - const sortedAudiencesByPercentScore = R.pipe( - R.sortWith([R.descend(R.prop('audience_total_percent_score'))]), + const sortedTeamsByPercentScore = R.pipe( + R.sortWith([R.descend(R.prop('team_total_percent_score'))]), R.take(10), - )(audiencesByPercentScore || []); - const percentScoreByAudienceData = [ + )(teamsByPercentScore || []); + const percentScoreByTeamData = [ { name: t('Percent of reached score'), - data: sortedAudiencesByPercentScore.map((a) => ({ - x: a.audience_name, - y: a.audience_total_percent_score, - fillColor: audiencesColors[a.audience_id], + data: sortedTeamsByPercentScore.map((a) => ({ + x: a.team_name, + y: a.team_total_percent_score, + fillColor: teamsColors[a.team_id], })), }, ]; @@ -300,16 +300,16 @@ const DashboardDefinitionStatistics = ({ - {t('Distribution of score by audience (in % of expectations)')} + {t('Distribution of score by team (in % of expectations)')} - {sortedAudiencesByPercentScore.length > 0 ? ( + {sortedTeamsByPercentScore.length > 0 ? ( ) : ( - {t('Audiences scores over time (in % of expectations)')} + {t('Teams scores over time (in % of expectations)')} - {audiencesPercentScoresData.length > 0 ? ( + {teamsPercentScoresData.length > 0 ? ( - {t('Distribution of total score by audience')} + {t('Distribution of total score by team')} - {audiencesTotalScores.length > 0 ? ( + {teamsTotalScores.length > 0 ? ( ) : ( - {t('Audiences scores over time')} + {t('Teams scores over time')} - {audiencesScores.length > 0 ? ( + {teamsScores.length > 0 ? ( + <> - + ); } } diff --git a/openex-front/src/admin/components/exercises/injects/InjectAddAudiences.js b/openex-front/src/admin/components/exercises/injects/InjectAddTeams.js similarity index 67% rename from openex-front/src/admin/components/exercises/injects/InjectAddAudiences.js rename to openex-front/src/admin/components/exercises/injects/InjectAddTeams.js index 35582cb1e5..7c28cbaf18 100644 --- a/openex-front/src/admin/components/exercises/injects/InjectAddAudiences.js +++ b/openex-front/src/admin/components/exercises/injects/InjectAddTeams.js @@ -3,13 +3,13 @@ import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; import * as R from 'ramda'; import { Button, Chip, List, ListItem, ListItemText, Dialog, DialogTitle, DialogContent, DialogActions, Box, ListItemIcon, Grid } from '@mui/material'; -import { CastForEducationOutlined, ControlPointOutlined } from '@mui/icons-material'; +import { GroupsOutlined, ControlPointOutlined } from '@mui/icons-material'; import withStyles from '@mui/styles/withStyles'; import SearchFilter from '../../../../components/SearchFilter'; import inject18n from '../../../../components/i18n'; import { storeHelper } from '../../../../actions/Schema'; -import { fetchAudiences } from '../../../../actions/Audience'; -import CreateAudience from '../audiences/CreateAudience'; +import { fetchTeams } from '../../../../actions/Team'; +import CreateTeam from '../../persons/teams/CreateTeam'; import { truncate } from '../../../../utils/String'; import { isExerciseReadOnly } from '../../../../utils/Exercise'; import { Transition } from '../../../../utils/Environment'; @@ -42,19 +42,19 @@ const styles = (theme) => ({ }, }); -class InjectAddAudiences extends Component { +class InjectAddTeams extends Component { constructor(props) { super(props); this.state = { open: false, keyword: '', - audiencesIds: [], + teamsIds: [], tags: [], }; } componentDidMount() { - this.props.fetchAudiences(this.props.exerciseId); + this.props.fetchTeams(this.props.exerciseId); } handleOpen() { @@ -62,10 +62,10 @@ class InjectAddAudiences extends Component { } handleClose() { - this.setState({ open: false, keyword: '', audiencesIds: [] }); + this.setState({ open: false, keyword: '', teamsIds: [] }); } - handleSearchAudiences(value) { + handleSearchTeams(value) { this.setState({ keyword: value }); } @@ -79,55 +79,55 @@ class InjectAddAudiences extends Component { this.setState({ tags: [] }); } - addAudience(audienceId) { + addTeam(teamId) { this.setState({ - audiencesIds: R.append(audienceId, this.state.audiencesIds), + teamsIds: R.append(teamId, this.state.teamsIds), }); } - removeAudience(audienceId) { + removeTeam(teamId) { this.setState({ - audiencesIds: R.filter((u) => u !== audienceId, this.state.audiencesIds), + teamsIds: R.filter((u) => u !== teamId, this.state.teamsIds), }); } - submitAddAudiences() { - this.props.handleAddAudiences(this.state.audiencesIds); + submitAddTeams() { + this.props.handleAddTeams(this.state.teamsIds); this.handleClose(); } onCreate(result) { - this.addAudience(result); + this.addTeam(result); } render() { const { classes, t, - audiences, - injectAudiencesIds, + teams, + injectTeamsIds, exerciseId, exercise, - audiencesMap, + teamsMap, } = this.props; - const { keyword, audiencesIds, tags } = this.state; + const { keyword, teamsIds, tags } = this.state; const filterByKeyword = (n) => keyword === '' - || (n.audience_name || '').toLowerCase().indexOf(keyword.toLowerCase()) + || (n.team_name || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 - || (n.audience_description || '') + || (n.team_description || '') .toLowerCase() .indexOf(keyword.toLowerCase()) !== -1; - const filteredAudiences = R.pipe( + const filteredTeams = R.pipe( R.filter( (n) => tags.length === 0 || R.any( - (filter) => R.includes(filter, n.audience_tags), + (filter) => R.includes(filter, n.team_tags), R.pluck('id', tags), ), ), R.filter(filterByKeyword), R.take(10), - )(audiences); + )(teams); return (
@@ -160,14 +160,14 @@ class InjectAddAudiences extends Component { }, }} > - {t('Add target audiences in this inject')} + {t('Add target teams in this inject')} @@ -181,36 +181,36 @@ class InjectAddAudiences extends Component { - {filteredAudiences.map((audience) => { - const disabled = audiencesIds.includes(audience.audience_id) - || injectAudiencesIds.includes(audience.audience_id); + {filteredTeams.map((team) => { + const disabled = teamsIds.includes(team.team_id) + || injectTeamsIds.includes(team.team_id); return ( - + ); })} - - {this.state.audiencesIds.map((audienceId) => { - const audience = audiencesMap[audienceId]; + {this.state.teamsIds.map((teamId) => { + const team = teamsMap[teamId]; return ( } + key={teamId} + onDelete={this.removeTeam.bind(this, teamId)} + label={truncate(team?.team_name || '', 22)} + icon={} classes={{ root: classes.chip }} /> ); @@ -239,7 +239,7 @@ class InjectAddAudiences extends Component { @@ -250,15 +250,15 @@ class InjectAddAudiences extends Component { } } -InjectAddAudiences.propTypes = { +InjectAddTeams.propTypes = { t: PropTypes.func, exerciseId: PropTypes.string, exercise: PropTypes.object, - fetchAudiences: PropTypes.func, - handleAddAudiences: PropTypes.func, + fetchTeams: PropTypes.func, + handleAddTeams: PropTypes.func, organizations: PropTypes.array, - audiences: PropTypes.array, - injectAudiencesIds: PropTypes.array, + teams: PropTypes.array, + injectTeamsIds: PropTypes.array, attachment: PropTypes.bool, }; @@ -266,13 +266,13 @@ const select = (state, ownProps) => { const helper = storeHelper(state); const { exerciseId } = ownProps; const exercise = helper.getExercise(exerciseId); - const audiences = helper.getExerciseAudiences(exerciseId); - const audiencesMap = helper.getAudiencesMap(); - return { exercise, audiences, audiencesMap }; + const teams = helper.getExerciseTeams(exerciseId); + const teamsMap = helper.getTeamsMap(); + return { exercise, teams, teamsMap }; }; export default R.compose( - connect(select, { fetchAudiences }), + connect(select, { fetchTeams }), inject18n, withStyles(styles), -)(InjectAddAudiences); +)(InjectAddTeams); diff --git a/openex-front/src/admin/components/exercises/injects/InjectDefinition.js b/openex-front/src/admin/components/exercises/injects/InjectDefinition.js index dd839bd565..78692309ec 100644 --- a/openex-front/src/admin/components/exercises/injects/InjectDefinition.js +++ b/openex-front/src/admin/components/exercises/injects/InjectDefinition.js @@ -23,7 +23,7 @@ import { ArrowDropDownOutlined, ArrowDropUpOutlined, AttachmentOutlined, - CastForEducationOutlined, + GroupsOutlined, CloseRounded, ControlPointOutlined, DeleteOutlined, @@ -33,15 +33,15 @@ import { import arrayMutators from 'final-form-arrays'; import { FieldArray } from 'react-final-form-arrays'; import inject18n from '../../../../components/i18n'; -import { fetchInjectAudiences, updateInject } from '../../../../actions/Inject'; +import { fetchInjectTeams, updateInject } from '../../../../actions/Inject'; import { fetchDocuments } from '../../../../actions/Document'; import { fetchExerciseArticles, fetchMedias } from '../../../../actions/Media'; import { fetchChallenges } from '../../../../actions/Challenge'; import ItemTags from '../../../../components/ItemTags'; import { storeHelper } from '../../../../actions/Schema'; -import AudiencePopover from '../audiences/AudiencePopover'; +import TeamPopover from '../../persons/teams/TeamPopover'; import ItemBoolean from '../../../../components/ItemBoolean'; -import InjectAddAudiences from './InjectAddAudiences'; +import InjectAddTeams from './InjectAddTeams'; import { isExerciseReadOnly } from '../../../../utils/Exercise'; import TextField from '../../../../components/TextField'; import SwitchField from '../../../../components/SwitchField'; @@ -94,7 +94,7 @@ const styles = (theme) => ({ title: { float: 'left', }, - allAudiences: { + allTeams: { float: 'right', marginTop: -7, }, @@ -113,25 +113,25 @@ const inlineStylesHeaders = { padding: 0, top: '0px', }, - audience_name: { + team_name: { float: 'left', width: '30%', fontSize: 12, fontWeight: '700', }, - audience_users_number: { + team_users_number: { float: 'left', width: '15%', fontSize: 12, fontWeight: '700', }, - audience_enabled: { + team_users_enabled_number: { float: 'left', width: '15%', fontSize: 12, fontWeight: '700', }, - audience_tags: { + team_tags: { float: 'left', width: '30%', fontSize: 12, @@ -204,7 +204,7 @@ const inlineStylesHeaders = { }; const inlineStyles = { - audience_name: { + team_name: { float: 'left', width: '30%', height: 20, @@ -212,7 +212,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_users_number: { + team_users_number: { float: 'left', width: '15%', height: 20, @@ -220,7 +220,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_enabled: { + team_users_enabled_number: { float: 'left', width: '15%', height: 20, @@ -228,7 +228,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_tags: { + team_tags: { float: 'left', width: '30%', height: 20, @@ -328,12 +328,12 @@ class InjectDefinition extends Component { constructor(props) { super(props); this.state = { - allAudiences: props.inject.inject_all_audiences, - audiencesIds: props.inject.inject_audiences, + allTeams: props.inject.inject_all_teams, + teamsIds: props.inject.inject_teams, documents: props.inject.inject_documents, expectations: props.inject.inject_content?.expectations || [], - audiencesSortBy: 'audience_name', - audiencesOrderAsc: true, + teamsSortBy: 'team_name', + teamsOrderAsc: true, documentsSortBy: 'document_name', documentsOrderAsc: true, articlesIds: props.inject.inject_content?.articles || [], @@ -349,14 +349,14 @@ class InjectDefinition extends Component { componentDidMount() { const { exerciseId, injectId } = this.props; this.props.fetchDocuments(); - this.props.fetchInjectAudiences(exerciseId, injectId); + this.props.fetchInjectTeams(exerciseId, injectId); this.props.fetchExerciseArticles(exerciseId); this.props.fetchMedias(); this.props.fetchChallenges(); } toggleAll() { - this.setState({ allAudiences: !this.state.allAudiences }); + this.setState({ allTeams: !this.state.allTeams }); } handleOpenVariables() { @@ -367,15 +367,15 @@ class InjectDefinition extends Component { this.setState({ openVariables: false }); } - handleAddAudiences(audiencesIds) { + handleAddTeams(teamsIds) { this.setState({ - audiencesIds: [...this.state.audiencesIds, ...audiencesIds], + teamsIds: [...this.state.teamsIds, ...teamsIds], }); } - handleRemoveAudience(audienceId) { + handleRemoveTeam(teamId) { this.setState({ - audiencesIds: this.state.audiencesIds.filter((a) => a !== audienceId), + teamsIds: this.state.teamsIds.filter((a) => a !== teamId), }); } @@ -438,17 +438,17 @@ class InjectDefinition extends Component { }); } - audiencesReverseBy(field) { + teamsReverseBy(field) { this.setState({ - audiencesSortBy: field, - audiencesOrderAsc: !this.state.audiencesOrderAsc, + teamsSortBy: field, + teamsOrderAsc: !this.state.teamsOrderAsc, }); } - audiencesSortHeader(field, label, isSortable) { + teamsSortHeader(field, label, isSortable) { const { t } = this.props; - const { audiencesSortBy, audiencesOrderAsc } = this.state; - const sortComponent = audiencesOrderAsc ? ( + const { teamsSortBy, teamsOrderAsc } = this.state; + const sortComponent = teamsOrderAsc ? ( ) : ( @@ -457,10 +457,10 @@ class InjectDefinition extends Component { return (
{t(label)} - {audiencesSortBy === field ? sortComponent : ''} + {teamsSortBy === field ? sortComponent : ''}
); } @@ -596,7 +596,7 @@ class InjectDefinition extends Component { } injectType.fields .filter( - (f) => !['audiences', 'articles', 'challenges', 'attachments', 'expectations'].includes( + (f) => !['teams', 'articles', 'challenges', 'attachments', 'expectations'].includes( f.key, ), ) @@ -644,7 +644,7 @@ class InjectDefinition extends Component { finalData[field.key] = data[field.key]; } }); - const { allAudiences, audiencesIds, documents } = this.state; + const { allTeams, teamsIds, documents } = this.state; const values = { inject_title: inject.inject_title, inject_contract: inject.inject_contract, @@ -653,8 +653,8 @@ class InjectDefinition extends Component { inject_depends_duration: inject.inject_depends_duration, inject_depends_from_another: inject.inject_depends_from_another, inject_content: finalData, - inject_all_audiences: allAudiences, - inject_audiences: audiencesIds, + inject_all_teams: allTeams, + inject_teams: teamsIds, inject_documents: documents, }; return this.props @@ -671,7 +671,7 @@ class InjectDefinition extends Component { if (injectType && Array.isArray(injectType.fields)) { injectType.fields .filter( - (f) => !['audiences', 'articles', 'challenges', 'attachments', 'expectations'].includes( + (f) => !['teams', 'articles', 'challenges', 'attachments', 'expectations'].includes( f.key, ), ) @@ -1041,7 +1041,7 @@ class InjectDefinition extends Component { exerciseId, exercise, injectTypes, - audiencesMap, + teamsMap, documentsMap, exercisesMap, tagsMap, @@ -1053,12 +1053,12 @@ class InjectDefinition extends Component { return ; } const { - allAudiences, - audiencesIds, + allTeams, + teamsIds, documents, expectations, - audiencesSortBy, - audiencesOrderAsc, + teamsSortBy, + teamsOrderAsc, documentsSortBy, documentsOrderAsc, articlesOrderAsc, @@ -1072,19 +1072,22 @@ class InjectDefinition extends Component { const injectType = R.head( injectTypes.filter((i) => i.contract_id === inject.inject_contract), ); - // -- AUDIENCES -- - const audiences = audiencesIds - .map((a) => audiencesMap[a]) + // -- TEAMS -- + const teams = teamsIds + .map((a) => teamsMap[a]) .filter((a) => a !== undefined); - const sortAudiences = R.sortWith( - audiencesOrderAsc - ? [R.ascend(R.prop(audiencesSortBy))] - : [R.descend(R.prop(audiencesSortBy))], + const sortTeams = R.sortWith( + teamsOrderAsc + ? [R.ascend(R.prop(teamsSortBy))] + : [R.descend(R.prop(teamsSortBy))], ); - const sortedAudiences = sortAudiences(audiences); - const hasAudiences = injectType.fields + const sortedTeams = sortTeams(teams.map((n) => ({ + team_users_enabled_number: exercise.exercise_teams_users.filter((o) => o.exercise_id === exerciseId && o.team_id === n.team_id).length, + ...n, + }))); + const hasTeams = injectType.fields .map((f) => f.key) - .includes('audiences'); + .includes('teams'); // -- ARTICLES -- const articles = articlesIds .map((a) => articlesMap[a]) @@ -1149,7 +1152,7 @@ class InjectDefinition extends Component { const initialValues = { ...inject.inject_content }; // Enrich initialValues with default contract value const builtInFields = [ - 'audiences', + 'teams', 'articles', 'challenges', 'attachments', @@ -1260,25 +1263,25 @@ class InjectDefinition extends Component { > {({ form, handleSubmit, submitting, values }) => ( - {hasAudiences && ( + {hasTeams && (
- {t('Targeted audiences')} + {t('Targeted teams')} } - label={{t('All audiences')}} + label={{t('All teams')}} />
@@ -1302,23 +1305,23 @@ class InjectDefinition extends Component { - {this.audiencesSortHeader( - 'audience_name', + {this.teamsSortHeader( + 'team_name', 'Name', true, )} - {this.audiencesSortHeader( - 'audience_users_number', + {this.teamsSortHeader( + 'team_users_number', 'Players', true, )} - {this.audiencesSortHeader( - 'audience_enabled', - 'Status', + {this.teamsSortHeader( + 'team_users_enabled_number', + 'Enabled players', true, )} - {this.audiencesSortHeader( - 'audience_tags', + {this.teamsSortHeader( + 'team_tags', 'Tags', true, )} @@ -1329,44 +1332,42 @@ class InjectDefinition extends Component {   - {allAudiences ? ( + {allTeams ? ( - +
- {t('All audiences')} + {t('All teams')}
- {exercise.exercise_users_number} + {exercise.exercise_all_users_number}
- + + {exercise.exercise_users_number} +
@@ -1379,62 +1380,54 @@ class InjectDefinition extends Component {
) : (
- {sortedAudiences.map((audience) => ( + {sortedTeams.map((team) => ( - +
- {audience.audience_name} + {team.team_name}
- {audience.audience_users_number} + {team.team_users_number}
- + {team.team_users_enabled_number}
} /> - ))} - @@ -1458,7 +1451,7 @@ class InjectDefinition extends Component {
{t('Media pressure to publish')} @@ -1576,7 +1569,7 @@ class InjectDefinition extends Component {
{t('Challenges to publish')} @@ -1679,7 +1672,7 @@ class InjectDefinition extends Component {
)} -
+
{t('Inject data')}
@@ -1968,7 +1961,7 @@ InjectDefinition.propTypes = { exercise: PropTypes.object, injectId: PropTypes.string, inject: PropTypes.object, - fetchInjectAudiences: PropTypes.func, + fetchInjectTeams: PropTypes.func, fetchExerciseArticles: PropTypes.func, fetchMedias: PropTypes.func, fetchChallenges: PropTypes.func, @@ -1985,14 +1978,14 @@ const select = (state, ownProps) => { const { injectId } = ownProps; const inject = helper.getInject(injectId); const documentsMap = helper.getDocumentsMap(); - const audiencesMap = helper.getAudiencesMap(); + const teamsMap = helper.getTeamsMap(); const mediasMap = helper.getMediasMap(); const articlesMap = helper.getArticlesMap(); const challengesMap = helper.getChallengesMap(); return { inject, documentsMap, - audiencesMap, + teamsMap, articlesMap, mediasMap, challengesMap, @@ -2001,7 +1994,7 @@ const select = (state, ownProps) => { export default R.compose( connect(select, { - fetchInjectAudiences, + fetchInjectTeams, updateInject, fetchDocuments, fetchExerciseArticles, diff --git a/openex-front/src/admin/components/exercises/injects/InjectPopover.js b/openex-front/src/admin/components/exercises/injects/InjectPopover.js index b8c4dd875e..3dd322de8b 100644 --- a/openex-front/src/admin/components/exercises/injects/InjectPopover.js +++ b/openex-front/src/admin/components/exercises/injects/InjectPopover.js @@ -250,8 +250,8 @@ class InjectPopover extends Component { 'inject_description', 'inject_tags', 'inject_content', - 'inject_audiences', - 'inject_all_audiences', + 'inject_teams', + 'inject_all_teams', 'inject_country', 'inject_city', ]), diff --git a/openex-front/src/admin/components/exercises/injects/Injects.js b/openex-front/src/admin/components/exercises/injects/Injects.js index c297303880..501c873af3 100644 --- a/openex-front/src/admin/components/exercises/injects/Injects.js +++ b/openex-front/src/admin/components/exercises/injects/Injects.js @@ -176,7 +176,7 @@ const Injects = () => { injectTypesMap, tagsMap, exercisesMap, - injectTypesWithNoAudiences, + injectTypesWithNoTeams, } = useHelper((helper) => { return { exercise: helper.getExercise(exerciseId), @@ -184,7 +184,7 @@ const Injects = () => { injectTypesMap: helper.getInjectTypesMap(), tagsMap: helper.getTagsMap(), exercisesMap: helper.getExercisesMap(), - injectTypesWithNoAudiences: helper.getInjectTypesWithNoAudiences(), + injectTypesWithNoTeams: helper.getInjectTypesWithNoTeams(), }; }); useDataLoader(() => { @@ -317,7 +317,7 @@ const Injects = () => { const duration = splitDuration(inject.inject_depends_duration || 0); const isDisabled = disabledTypes.includes(inject.inject_type) || !types.includes(inject.inject_type); - const isNoAudience = injectTypesWithNoAudiences.includes( + const isNoTeam = injectTypesWithNoTeams.includes( inject.inject_type, ); let injectStatus = inject.inject_enabled @@ -382,7 +382,7 @@ const Injects = () => { className={classes.bodyItem} style={inlineStyles.inject_users_number} > - {isNoAudience ? t('N/A') : inject.inject_users_number} + {isNoTeam ? t('N/A') : inject.inject_users_number}
({ title: { float: 'left', }, - allAudiences: { + allTeams: { float: 'right', marginTop: -7, }, @@ -117,25 +117,25 @@ const inlineStylesHeaders = { padding: 0, top: '0px', }, - audience_name: { + team_name: { float: 'left', width: '30%', fontSize: 12, fontWeight: '700', }, - audience_users_number: { + team_users_number: { float: 'left', width: '15%', fontSize: 12, fontWeight: '700', }, - audience_enabled: { + team_enabled: { float: 'left', width: '15%', fontSize: 12, fontWeight: '700', }, - audience_tags: { + team_tags: { float: 'left', width: '30%', fontSize: 12, @@ -208,7 +208,7 @@ const inlineStylesHeaders = { }; const inlineStyles = { - audience_name: { + team_name: { float: 'left', width: '30%', height: 20, @@ -216,7 +216,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_users_number: { + team_users_number: { float: 'left', width: '15%', height: 20, @@ -224,7 +224,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_enabled: { + team_enabled: { float: 'left', width: '15%', height: 20, @@ -232,7 +232,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_tags: { + team_tags: { float: 'left', width: '30%', height: 20, @@ -332,12 +332,12 @@ class QuickInject extends Component { constructor(props) { super(props); this.state = { - allAudiences: false, - audiencesIds: [], + allTeams: false, + teamsIds: [], documents: [], expectations: [], - audiencesSortBy: 'audience_name', - audiencesOrderAsc: true, + teamsSortBy: 'team_name', + teamsOrderAsc: true, documentsSortBy: 'document_name', documentsOrderAsc: true, articlesIds: [], @@ -359,7 +359,7 @@ class QuickInject extends Component { } toggleAll() { - this.setState({ allAudiences: !this.state.allAudiences }); + this.setState({ allTeams: !this.state.allTeams }); } handleOpenVariables() { @@ -370,15 +370,15 @@ class QuickInject extends Component { this.setState({ openVariables: false }); } - handleAddAudiences(audiencesIds) { + handleAddTeams(teamsIds) { this.setState({ - audiencesIds: [...this.state.audiencesIds, ...audiencesIds], + teamsIds: [...this.state.teamsIds, ...teamsIds], }); } - handleRemoveAudience(audienceId) { + handleRemoveTeam(teamId) { this.setState({ - audiencesIds: this.state.audiencesIds.filter((a) => a !== audienceId), + teamsIds: this.state.teamsIds.filter((a) => a !== teamId), }); } @@ -441,17 +441,17 @@ class QuickInject extends Component { }); } - audiencesReverseBy(field) { + teamsReverseBy(field) { this.setState({ - audiencesSortBy: field, - audiencesOrderAsc: !this.state.audiencesOrderAsc, + teamsSortBy: field, + teamsOrderAsc: !this.state.teamsOrderAsc, }); } - audiencesSortHeader(field, label, isSortable) { + teamsSortHeader(field, label, isSortable) { const { t } = this.props; - const { audiencesSortBy, audiencesOrderAsc } = this.state; - const sortComponent = audiencesOrderAsc ? ( + const { teamsSortBy, teamsOrderAsc } = this.state; + const sortComponent = teamsOrderAsc ? ( ) : ( @@ -460,10 +460,10 @@ class QuickInject extends Component { return (
{t(label)} - {audiencesSortBy === field ? sortComponent : ''} + {teamsSortBy === field ? sortComponent : ''}
); } @@ -599,7 +599,7 @@ class QuickInject extends Component { } injectType.fields .filter( - (f) => !['audiences', 'articles', 'challenges', 'attachments', 'expectations'].includes( + (f) => !['teams', 'articles', 'challenges', 'attachments', 'expectations'].includes( f.key, ), ) @@ -647,7 +647,7 @@ class QuickInject extends Component { finalData[field.key] = data[field.key]; } }); - const { allAudiences, audiencesIds, documents } = this.state; + const { allTeams, teamsIds, documents } = this.state; const injectDependsDuration = secondsFromToNow( this.props.exercise.exercise_start_date, ); @@ -657,8 +657,8 @@ class QuickInject extends Component { inject_depends_duration: injectDependsDuration > 0 ? injectDependsDuration : 0, inject_content: finalData, - inject_all_audiences: allAudiences, - inject_audiences: audiencesIds, + inject_all_teams: allTeams, + inject_teams: teamsIds, inject_documents: documents, }; return this.props @@ -675,7 +675,7 @@ class QuickInject extends Component { if (injectType && Array.isArray(injectType.fields)) { injectType.fields .filter( - (f) => !['audiences', 'articles', 'challenges', 'attachments', 'expectations'].includes( + (f) => !['teams', 'articles', 'challenges', 'attachments', 'expectations'].includes( f.key, ), ) @@ -1038,7 +1038,7 @@ class QuickInject extends Component { exerciseId, exercise, injectTypes, - audiencesMap, + teamsMap, documentsMap, exercisesMap, tagsMap, @@ -1047,12 +1047,12 @@ class QuickInject extends Component { challengesMap, } = this.props; const { - allAudiences, - audiencesIds, + allTeams, + teamsIds, documents, expectations, - audiencesSortBy, - audiencesOrderAsc, + teamsSortBy, + teamsOrderAsc, documentsSortBy, documentsOrderAsc, articlesOrderAsc, @@ -1066,19 +1066,19 @@ class QuickInject extends Component { const injectType = R.head( injectTypes.filter((i) => i.contract_id === EMAIL_CONTRACT), ); - // -- AUDIENCES -- - const audiences = audiencesIds - .map((a) => audiencesMap[a]) + // -- TEAMS -- + const teams = teamsIds + .map((a) => teamsMap[a]) .filter((a) => a !== undefined); - const sortAudiences = R.sortWith( - audiencesOrderAsc - ? [R.ascend(R.prop(audiencesSortBy))] - : [R.descend(R.prop(audiencesSortBy))], + const sortTeams = R.sortWith( + teamsOrderAsc + ? [R.ascend(R.prop(teamsSortBy))] + : [R.descend(R.prop(teamsSortBy))], ); - const sortedAudiences = sortAudiences(audiences); - const hasAudiences = injectType.fields + const sortedTeams = sortTeams(teams); + const hasTeams = injectType.fields .map((f) => f.key) - .includes('audiences'); + .includes('teams'); // -- ARTICLES -- const articles = articlesIds .map((a) => articlesMap[a]) @@ -1142,7 +1142,7 @@ class QuickInject extends Component { const initialValues = {}; // Enrich initialValues with default contract value const builtInFields = [ - 'audiences', + 'teams', 'articles', 'challenges', 'attachments', @@ -1251,25 +1251,25 @@ class QuickInject extends Component { > {({ form, handleSubmit, submitting, values }) => ( - {hasAudiences && ( + {hasTeams && (
- {t('Targeted audiences')} + {t('Targeted teams')} } - label={{t('All audiences')}} + label={{t('All teams')}} />
@@ -1293,23 +1293,23 @@ class QuickInject extends Component { - {this.audiencesSortHeader( - 'audience_name', + {this.teamsSortHeader( + 'team_name', 'Name', true, )} - {this.audiencesSortHeader( - 'audience_users_number', + {this.teamsSortHeader( + 'team_users_number', 'Players', true, )} - {this.audiencesSortHeader( - 'audience_enabled', + {this.teamsSortHeader( + 'team_enabled', 'Status', true, )} - {this.audiencesSortHeader( - 'audience_tags', + {this.teamsSortHeader( + 'team_tags', 'Tags', true, )} @@ -1320,7 +1320,7 @@ class QuickInject extends Component {   - {allAudiences ? ( + {allTeams ? (
- {t('All audiences')} + {t('All teams')}
{exercise.exercise_users_number} @@ -1347,7 +1347,7 @@ class QuickInject extends Component {
@@ -1370,9 +1370,9 @@ class QuickInject extends Component { ) : (
- {sortedAudiences.map((audience) => ( + {sortedTeams.map((team) => ( @@ -1384,24 +1384,24 @@ class QuickInject extends Component {
- {audience.audience_name} + {team.team_name}
- {audience.audience_users_number} + {team.team_users_number}
} /> - + {isExerciseUpdatable(exercise) + ? () :   } ))} - @@ -1449,7 +1448,7 @@ class QuickInject extends Component {
{t('Media pressure to publish')} @@ -1567,7 +1566,7 @@ class QuickInject extends Component {
{t('Challenges to publish')} @@ -1670,7 +1669,7 @@ class QuickInject extends Component {
)} -
+
{t('Inject data')}
@@ -1957,7 +1956,7 @@ QuickInject.propTypes = { nsdt: PropTypes.func, exerciseId: PropTypes.string, exercise: PropTypes.object, - fetchInjectAudiences: PropTypes.func, + fetchInjectTeams: PropTypes.func, fetchExerciseArticles: PropTypes.func, fetchMedias: PropTypes.func, fetchChallenges: PropTypes.func, @@ -1972,13 +1971,13 @@ QuickInject.propTypes = { const select = (state) => { const helper = storeHelper(state); const documentsMap = helper.getDocumentsMap(); - const audiencesMap = helper.getAudiencesMap(); + const teamsMap = helper.getTeamsMap(); const mediasMap = helper.getMediasMap(); const articlesMap = helper.getArticlesMap(); const challengesMap = helper.getChallengesMap(); return { documentsMap, - audiencesMap, + teamsMap, articlesMap, mediasMap, challengesMap, diff --git a/openex-front/src/admin/components/exercises/injects/expectations/Expectation.d.ts b/openex-front/src/admin/components/exercises/injects/expectations/Expectation.d.ts index 3e16f1fdcd..e35e697aad 100644 --- a/openex-front/src/admin/components/exercises/injects/expectations/Expectation.d.ts +++ b/openex-front/src/admin/components/exercises/injects/expectations/Expectation.d.ts @@ -1,7 +1,7 @@ import type { InjectExpectation } from '../../../../../utils/api-types'; -export interface InjectExpectationsStore extends Omit { - inject_expectation_audience: string | undefined; +export interface InjectExpectationsStore extends Omit { + inject_expectation_team: string | undefined; inject_expectation_article: string | undefined; inject_expectation_challenge: string | undefined; } diff --git a/openex-front/src/admin/components/exercises/injects/expectations/ExpectationFormUtils.ts b/openex-front/src/admin/components/exercises/injects/expectations/ExpectationFormUtils.ts index d61f3917a4..2bbb0e7a9f 100644 --- a/openex-front/src/admin/components/exercises/injects/expectations/ExpectationFormUtils.ts +++ b/openex-front/src/admin/components/exercises/injects/expectations/ExpectationFormUtils.ts @@ -6,7 +6,7 @@ import type { ExpectationInput } from './Expectation'; export const infoMessage = (type: string, t: (key: string) => string) => { if (type === 'ARTICLE') { - return t('This expectation is handled automatically by the platform and triggered when audience reads articles'); + return t('This expectation is handled automatically by the platform and triggered when team reads articles'); } return ''; }; diff --git a/openex-front/src/admin/components/exercises/lessons/Lessons.js b/openex-front/src/admin/components/exercises/lessons/Lessons.js index d8bcd9b310..eec3fa852d 100644 --- a/openex-front/src/admin/components/exercises/lessons/Lessons.js +++ b/openex-front/src/admin/components/exercises/lessons/Lessons.js @@ -49,9 +49,8 @@ import { resetLessonsAnswers, sendLessons, } from '../../../../actions/Lessons'; -import { fetchAudiences } from '../../../../actions/Audience'; import { resolveUserName } from '../../../../utils/String'; -import { updateExerciseLessons } from '../../../../actions/Exercise'; +import { fetchExerciseTeams, updateExerciseLessons } from '../../../../actions/Exercise'; import SendLessonsForm from './SendLessonsForm'; import { fetchPlayers } from '../../../../actions/User'; import LessonsObjectives from './LessonsObjectives'; @@ -120,7 +119,7 @@ const Lessons = () => { exercise, objectives, injects, - audiencesMap, + teamsMap, lessonsCategories, lessonsQuestions, lessonsAnswers, @@ -131,7 +130,7 @@ const Lessons = () => { exercise: helper.getExercise(exerciseId), objectives: helper.getExerciseObjectives(exerciseId), injects: helper.getExerciseInjects(exerciseId), - audiencesMap: helper.getAudiencesMap(), + teamsMap: helper.getTeamsMap(), lessonsCategories: helper.getExerciseLessonsCategories(exerciseId), lessonsQuestions: helper.getExerciseLessonsQuestions(exerciseId), lessonsAnswers: helper.getExerciseLessonsAnswers(exerciseId), @@ -146,7 +145,7 @@ const Lessons = () => { dispatch(fetchLessonsAnswers(exerciseId)); dispatch(fetchObjectives(exerciseId)); dispatch(fetchInjects(exerciseId)); - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); dispatch(fetchPlayers()); }); const applyTemplate = () => { @@ -251,7 +250,7 @@ const Lessons = () => { {t('hours')} - {t('Audience')} + {t('Team')} {exercise.exercise_users_number} {t('players')} @@ -363,7 +362,7 @@ const Lessons = () => { lessonsAnswers={lessonsAnswers} setSelectedQuestion={setSelectedQuestion} lessonsQuestions={lessonsQuestions} - audiencesMap={audiencesMap} + teamsMap={teamsMap} /> ({ paper: { @@ -36,7 +36,7 @@ const LessonsCategories = ({ lessonsAnswers, lessonsQuestions, setSelectedQuestion, - audiencesMap, + teamsMap, isReport, }) => { const classes = useStyles(); @@ -49,10 +49,10 @@ const LessonsCategories = ({ R.ascend(R.prop('lessons_question_order')), ]); const sortedCategories = sortCategories(lessonsCategories); - const handleUpdateAudiences = (lessonsCategoryId, audiencesIds) => { - const data = { lessons_category_audiences: audiencesIds }; + const handleUpdateTeams = (lessonsCategoryId, teamsIds) => { + const data = { lessons_category_teams: teamsIds }; return dispatch( - updateLessonsCategoryAudiences(exerciseId, lessonsCategoryId, data), + updateLessonsCategoryTeams(exerciseId, lessonsCategoryId, data), ); }; const consolidatedAnswers = R.pipe( @@ -209,16 +209,16 @@ const LessonsCategories = ({ - {t('Targeted audiences')} + {t('Targeted teams')} {!isReport && ( - )}
@@ -226,26 +226,26 @@ const LessonsCategories = ({ variant="outlined" classes={{ root: classes.paperPadding }} > - {category.lessons_category_audiences.map((audienceId) => { - const audience = audiencesMap[audienceId]; + {category.lessons_category_teams.map((teamId) => { + const team = teamsMap[teamId]; return ( handleUpdateAudiences( + : () => handleUpdateTeams( category.lessonscategory_id, R.filter( - (n) => n !== audienceId, - category.lessons_category_audiences, + (n) => n !== teamId, + category.lessons_category_teams, ), ) } - label={truncate(audience?.audience_name || '', 30)} + label={truncate(team?.team_name || '', 30)} icon={} classes={{ root: classes.chip }} /> diff --git a/openex-front/src/admin/components/exercises/lessons/categories/LessonsCategoryAddAudiences.js b/openex-front/src/admin/components/exercises/lessons/categories/LessonsCategoryAddTeams.js similarity index 66% rename from openex-front/src/admin/components/exercises/lessons/categories/LessonsCategoryAddAudiences.js rename to openex-front/src/admin/components/exercises/lessons/categories/LessonsCategoryAddTeams.js index 88ab25937c..3e7cd6363f 100644 --- a/openex-front/src/admin/components/exercises/lessons/categories/LessonsCategoryAddAudiences.js +++ b/openex-front/src/admin/components/exercises/lessons/categories/LessonsCategoryAddTeams.js @@ -8,8 +8,8 @@ import withStyles from '@mui/styles/withStyles'; import SearchFilter from '../../../../../components/SearchFilter'; import inject18n from '../../../../../components/i18n'; import { storeHelper } from '../../../../../actions/Schema'; -import { fetchAudiences } from '../../../../../actions/Audience'; -import CreateAudience from '../../audiences/CreateAudience'; +import { fetchTeams } from '../../../../../actions/Team'; +import CreateTeam from '../../../persons/teams/CreateTeam'; import { truncate } from '../../../../../utils/String'; import { Transition } from '../../../../../utils/Environment'; import TagsFilter from '../../../../../components/TagsFilter'; @@ -40,19 +40,19 @@ const styles = (theme) => ({ }, }); -class LessonsCategoryAddAudiences extends Component { +class LessonsCategoryAddTeams extends Component { constructor(props) { super(props); this.state = { open: false, keyword: '', - audiencesIds: [], + teamsIds: [], tags: [], }; } componentDidMount() { - this.props.fetchAudiences(this.props.exerciseId); + this.props.fetchTeams(this.props.exerciseId); } handleOpen() { @@ -60,10 +60,10 @@ class LessonsCategoryAddAudiences extends Component { } handleClose() { - this.setState({ open: false, keyword: '', audiencesIds: [] }); + this.setState({ open: false, keyword: '', teamsIds: [] }); } - handleSearchAudiences(value) { + handleSearchTeams(value) { this.setState({ keyword: value }); } @@ -77,73 +77,73 @@ class LessonsCategoryAddAudiences extends Component { this.setState({ tags: [] }); } - addAudience(audienceId) { + addTeam(teamId) { this.setState({ - audiencesIds: R.append(audienceId, this.state.audiencesIds), + teamsIds: R.append(teamId, this.state.teamsIds), }); } - addAllAudiences() { - const { lessonsCategoryAudiencesIds, audiences } = this.props; - const audiencesToAdd = R.pipe( - R.map((n) => n.audience_id), - R.filter((n) => !lessonsCategoryAudiencesIds.includes(n)), - )(audiences); + addAllTeams() { + const { lessonsCategoryTeamsIds, teams } = this.props; + const teamsToAdd = R.pipe( + R.map((n) => n.team_id), + R.filter((n) => !lessonsCategoryTeamsIds.includes(n)), + )(teams); this.setState({ - audiencesIds: audiencesToAdd, + teamsIds: teamsToAdd, }); } - removeAudience(audienceId) { + removeTeam(teamId) { this.setState({ - audiencesIds: R.filter((u) => u !== audienceId, this.state.audiencesIds), + teamsIds: R.filter((u) => u !== teamId, this.state.teamsIds), }); } - submitAddAudiences() { + submitAddTeams() { const { - lessonsCategoryAudiencesIds, + lessonsCategoryTeamsIds, lessonsCategoryId, - handleUpdateAudiences, + handleUpdateTeams, } = this.props; - handleUpdateAudiences(lessonsCategoryId, [ - ...lessonsCategoryAudiencesIds, - ...this.state.audiencesIds, + handleUpdateTeams(lessonsCategoryId, [ + ...lessonsCategoryTeamsIds, + ...this.state.teamsIds, ]); this.handleClose(); } onCreate(result) { - this.addAudience(result); + this.addTeam(result); } render() { const { classes, t, - audiences, - lessonsCategoryAudiencesIds, + teams, + lessonsCategoryTeamsIds, exerciseId, - audiencesMap, + teamsMap, } = this.props; - const { keyword, audiencesIds, tags } = this.state; + const { keyword, teamsIds, tags } = this.state; const filterByKeyword = (n) => keyword === '' - || (n.audience_name || '').toLowerCase().indexOf(keyword.toLowerCase()) + || (n.team_name || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 - || (n.audience_description || '') + || (n.team_description || '') .toLowerCase() .indexOf(keyword.toLowerCase()) !== -1; - const filteredAudiences = R.pipe( + const filteredTeams = R.pipe( R.filter( (n) => tags.length === 0 || R.any( - (filter) => R.includes(filter, n.audience_tags), + (filter) => R.includes(filter, n.team_tags), R.pluck('id', tags), ), ), R.filter(filterByKeyword), R.take(10), - )(audiences); + )(teams); return (
- {t('Add target audiences in this lessons learned category')} + {t('Add target teams in this lessons learned category')}
@@ -274,28 +274,28 @@ class LessonsCategoryAddAudiences extends Component { } } -LessonsCategoryAddAudiences.propTypes = { +LessonsCategoryAddTeams.propTypes = { t: PropTypes.func, exerciseId: PropTypes.string, - fetchAudiences: PropTypes.func, - handleUpdateAudiences: PropTypes.func, + fetchTeams: PropTypes.func, + handleUpdateTeams: PropTypes.func, organizations: PropTypes.array, - audiences: PropTypes.array, + teams: PropTypes.array, lessonsCategoryId: PropTypes.string, - lessonsCategoryAudiencesIds: PropTypes.array, + lessonsCategoryTeamsIds: PropTypes.array, attachment: PropTypes.bool, }; const select = (state, ownProps) => { const helper = storeHelper(state); const { exerciseId } = ownProps; - const audiences = helper.getExerciseAudiences(exerciseId); - const audiencesMap = helper.getAudiencesMap(); - return { audiences, audiencesMap }; + const teams = helper.getExerciseTeams(exerciseId); + const teamsMap = helper.getTeamsMap(); + return { teams, teamsMap }; }; export default R.compose( - connect(select, { fetchAudiences }), + connect(select, { fetchTeams }), inject18n, withStyles(styles), -)(LessonsCategoryAddAudiences); +)(LessonsCategoryAddTeams); diff --git a/openex-front/src/admin/components/exercises/mails/Inject.js b/openex-front/src/admin/components/exercises/mails/Inject.js index 7fcadea5fb..26cf093347 100644 --- a/openex-front/src/admin/components/exercises/mails/Inject.js +++ b/openex-front/src/admin/components/exercises/mails/Inject.js @@ -191,7 +191,7 @@ const Inject = () => { {t('Documents')} - {t('Audiences')} + {t('Teams')} diff --git a/openex-front/src/admin/components/exercises/reports/Report.js b/openex-front/src/admin/components/exercises/reports/Report.js index 7c63f06dcf..67dd4dae75 100644 --- a/openex-front/src/admin/components/exercises/reports/Report.js +++ b/openex-front/src/admin/components/exercises/reports/Report.js @@ -7,11 +7,10 @@ import { useDispatch } from 'react-redux'; import { useFormatter } from '../../../../components/i18n'; import { useHelper } from '../../../../store'; import useDataLoader from '../../../../utils/ServerSideEvent'; -import { fetchAudiences } from '../../../../actions/Audience'; import ResultsMenu from '../ResultsMenu'; import { fetchInjects, fetchInjectTypes } from '../../../../actions/Inject'; import { fetchExerciseChallenges } from '../../../../actions/Challenge'; -import { fetchExerciseInjectExpectations } from '../../../../actions/Exercise'; +import { fetchExerciseInjectExpectations, fetchExerciseTeams } from '../../../../actions/Exercise'; import { fetchPlayers } from '../../../../actions/User'; import { fetchOrganizations } from '../../../../actions/Organization'; import { fetchExerciseCommunications } from '../../../../actions/Communication'; @@ -79,11 +78,11 @@ const Dashboard = () => { const { report, exercise, - audiences, + teams, injects, challengesMap, injectTypesMap, - audiencesMap, + teamsMap, injectExpectations, injectsMap, usersMap, @@ -98,8 +97,8 @@ const Dashboard = () => { return { report: helper.getReport(reportId), exercise: helper.getExercise(exerciseId), - audiences: helper.getExerciseAudiences(exerciseId), - audiencesMap: helper.getAudiencesMap(), + teams: helper.getExerciseTeams(exerciseId), + teamsMap: helper.getTeamsMap(), injects: helper.getExerciseInjects(exerciseId), injectsMap: helper.getInjectsMap(), usersMap: helper.getUsersMap(), @@ -117,7 +116,7 @@ const Dashboard = () => { }); useDataLoader(() => { dispatch(fetchReports(exerciseId)); - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); dispatch(fetchInjectTypes()); dispatch(fetchInjects(exerciseId)); dispatch(fetchExerciseChallenges(exerciseId)); @@ -182,9 +181,9 @@ const Dashboard = () => { sx={{ fontSize: 50 }} />
-
{t('Audiences')}
+
{t('Teams')}
- {(audiences || []).length} + {(teams || []).length}
@@ -212,14 +211,14 @@ const Dashboard = () => { )} {report.report_stats_definition && ( )} {report.report_stats_definition_score && ( { )} {report.report_stats_data && ( { { lessonsAnswers={lessonsAnswers} isReport={true} lessonsQuestions={lessonsQuestions} - audiencesMap={audiencesMap} + teamsMap={teamsMap} /> )}
diff --git a/openex-front/src/admin/components/exercises/teams/ExerciseAddTeams.js b/openex-front/src/admin/components/exercises/teams/ExerciseAddTeams.js new file mode 100644 index 0000000000..10aea20bac --- /dev/null +++ b/openex-front/src/admin/components/exercises/teams/ExerciseAddTeams.js @@ -0,0 +1,263 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import * as R from 'ramda'; +import { Button, Slide, Chip, Avatar, List, ListItem, ListItemText, Dialog, DialogTitle, DialogContent, DialogActions, Box, ListItemIcon, Grid, Fab } from '@mui/material'; +import { Add, GroupsOutlined } from '@mui/icons-material'; +import withStyles from '@mui/styles/withStyles'; +import { addExerciseTeams } from '../../../../actions/Exercise'; +import SearchFilter from '../../../../components/SearchFilter'; +import inject18n from '../../../../components/i18n'; +import { storeHelper } from '../../../../actions/Schema'; +import { fetchTeams } from '../../../../actions/Team'; +import CreateTeam from '../../persons/teams/CreateTeam'; +import { truncate } from '../../../../utils/String'; +import ItemTags from '../../../../components/ItemTags'; +import TagsFilter from '../../../../components/TagsFilter'; + +const styles = () => ({ + createButton: { + position: 'fixed', + bottom: 30, + right: 230, + }, + box: { + width: '100%', + minHeight: '100%', + padding: 20, + border: '1px dashed rgba(255, 255, 255, 0.3)', + }, + chip: { + margin: '0 10px 10px 0', + }, +}); + +const Transition = React.forwardRef((props, ref) => ( + +)); +Transition.displayName = 'TransitionSlide'; + +class TeamAddTeams extends Component { + constructor(props) { + super(props); + this.state = { + open: false, + keyword: '', + teamsIds: [], + tags: [], + }; + } + + componentDidMount() { + this.props.fetchTeams(); + } + + handleOpen() { + this.setState({ open: true }); + } + + handleClose() { + this.setState({ open: false, keyword: '', teamsIds: [] }); + } + + handleSearchTeams(value) { + this.setState({ keyword: value }); + } + + handleAddTag(value) { + if (value) { + this.setState({ tags: [value] }); + } + } + + handleClearTag() { + this.setState({ tags: [] }); + } + + addTeam(teamId) { + this.setState({ teamsIds: R.append(teamId, this.state.teamsIds) }); + } + + removeTeam(teamId) { + this.setState({ + teamsIds: R.filter((u) => u !== teamId, this.state.teamsIds), + }); + } + + submitAddTeams() { + this.props.addExerciseTeams( + this.props.exerciseId, + { + exercise_teams: R.uniq([ + ...this.state.teamsIds, + ]), + }, + ); + this.handleClose(); + } + + onCreate(result) { + this.addTeam(result); + } + + render() { + const { + classes, + t, + teamsMap, + exerciseTeamsIds, + organizationsMap, + } = this.props; + const { keyword, teamsIds, tags } = this.state; + const filterByKeyword = (n) => keyword === '' + || (n.team_name || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.team_description || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.organization_name || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.organization_description || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1; + const filteredTeams = R.pipe( + R.map((u) => ({ + organization_name: + organizationsMap[u.team_organization]?.organization_name ?? '-', + organization_description: + organizationsMap[u.team_organization]?.organization_description + ?? '-', + ...u, + })), + R.filter( + (n) => tags.length === 0 + || R.any( + (filter) => R.includes(filter, n.team_tags), + R.pluck('id', tags), + ), + ), + R.filter(filterByKeyword), + R.take(10), + )(R.values(teamsMap)); + return ( + <> + + + + + {t('Add teams in this exercise')} + + + + + + + + + + + + + {filteredTeams.map((team) => { + const disabled = teamsIds.includes(team.team_id) + || exerciseTeamsIds.includes(team.team_id); + return ( + + + + + + + + ); + })} + + + + + + {this.state.teamsIds.map((teamId) => { + const team = teamsMap[teamId]; + const teamGravatar = R.propOr('-', 'team_gravatar', team); + return ( + } + classes={{ root: classes.chip }} + /> + ); + })} + + + + + + + + + + + ); + } +} + +TeamAddTeams.propTypes = { + t: PropTypes.func, + exerciseId: PropTypes.string, + addExerciseTeams: PropTypes.func, + fetchTeams: PropTypes.func, + organizations: PropTypes.array, + teamsMap: PropTypes.object, + exerciseTeamsIds: PropTypes.array, +}; + +const select = (state) => { + const helper = storeHelper(state); + return { + teamsMap: helper.getTeamsMap(), + organizationsMap: helper.getOrganizationsMap(), + }; +}; + +export default R.compose( + connect(select, { addExerciseTeams, fetchTeams }), + inject18n, + withStyles(styles), +)(TeamAddTeams); diff --git a/openex-front/src/admin/components/exercises/audiences/Audiences.js b/openex-front/src/admin/components/exercises/teams/Teams.js similarity index 68% rename from openex-front/src/admin/components/exercises/audiences/Audiences.js rename to openex-front/src/admin/components/exercises/teams/Teams.js index 91e65fb0d1..b501149962 100644 --- a/openex-front/src/admin/components/exercises/audiences/Audiences.js +++ b/openex-front/src/admin/components/exercises/teams/Teams.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { makeStyles } from '@mui/styles'; import { List, ListItem, Drawer, ListItemIcon, ListItemText, ListItemSecondaryAction, Tooltip, IconButton } from '@mui/material'; import { useDispatch } from 'react-redux'; -import { CastForEducationOutlined, FileDownloadOutlined } from '@mui/icons-material'; +import { GroupsOutlined, FileDownloadOutlined } from '@mui/icons-material'; import { useParams } from 'react-router-dom'; import { CSVLink } from 'react-csv'; import { useFormatter } from '../../../../components/i18n'; @@ -10,16 +10,15 @@ import useDataLoader from '../../../../utils/ServerSideEvent'; import ItemTags from '../../../../components/ItemTags'; import SearchFilter from '../../../../components/SearchFilter'; import TagsFilter from '../../../../components/TagsFilter'; -import { fetchAudiences } from '../../../../actions/Audience'; -import CreateAudience from './CreateAudience'; -import AudiencePopover from './AudiencePopover'; -import ItemBoolean from '../../../../components/ItemBoolean'; -import AudiencePlayers from './AudiencePlayers'; +import { fetchExerciseTeams } from '../../../../actions/Exercise'; +import TeamPopover from '../../persons/teams/TeamPopover'; +import TeamPlayers from '../../persons/teams/TeamPlayers'; import { useHelper } from '../../../../store'; import useSearchAnFilter from '../../../../utils/SortingFiltering'; import { usePermissions } from '../../../../utils/Exercise'; import { exportData } from '../../../../utils/Environment'; import DefinitionMenu from '../DefinitionMenu'; +import ExerciseAddTeams from './ExerciseAddTeams'; const useStyles = makeStyles(() => ({ container: { @@ -51,31 +50,37 @@ const headerStyles = { padding: 0, top: '0px', }, - audience_name: { + team_name: { float: 'left', width: '20%', fontSize: 12, fontWeight: '700', }, - audience_description: { + team_description: { float: 'left', width: '25%', fontSize: 12, fontWeight: '700', }, - audience_users_number: { + team_users_number: { float: 'left', - width: '10%', + width: '12%', fontSize: 12, fontWeight: '700', }, - audience_enabled: { + team_users_enabled_number: { + float: 'left', + width: '12%', + fontSize: 12, + fontWeight: '700', + }, + team_enabled: { float: 'left', width: '15%', fontSize: 12, fontWeight: '700', }, - audience_tags: { + team_tags: { float: 'left', width: '30%', fontSize: 12, @@ -84,7 +89,7 @@ const headerStyles = { }; const inlineStyles = { - audience_name: { + team_name: { float: 'left', width: '20%', height: 20, @@ -92,7 +97,7 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_description: { + team_description: { float: 'left', width: '25%', height: 20, @@ -100,23 +105,23 @@ const inlineStyles = { overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_users_number: { + team_users_number: { float: 'left', - width: '10%', + width: '12%', height: 20, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_enabled: { + team_users_enabled_number: { float: 'left', - width: '15%', + width: '12%', height: 20, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }, - audience_tags: { + team_tags: { float: 'left', width: '30%', height: 20, @@ -126,29 +131,32 @@ const inlineStyles = { }, }; -const Audiences = () => { +const Teams = () => { // Standard hooks const classes = useStyles(); const dispatch = useDispatch(); const { t } = useFormatter(); - const [selectedAudience, setSelectedAudience] = useState(null); + const [selectedTeam, setSelectedTeam] = useState(null); // Filter and sort hook - const filtering = useSearchAnFilter('audience', 'name', [ + const filtering = useSearchAnFilter('team', 'name', [ 'name', 'description', ]); // Fetching data const { exerciseId } = useParams(); const permissions = usePermissions(exerciseId); - const { exercise, audiences, tagsMap } = useHelper((helper) => ({ + const { exercise, teams, tagsMap } = useHelper((helper) => ({ exercise: helper.getExercise(exerciseId), - audiences: helper.getExerciseAudiences(exerciseId), + teams: helper.getExerciseTeams(exerciseId), tagsMap: helper.getTagsMap(), })); useDataLoader(() => { - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); }); - const sortedAudiences = filtering.filterAndSort(audiences); + const sortedTeams = filtering.filterAndSort(teams.map((n) => ({ + team_users_enabled_number: exercise.exercise_teams_users.filter((o) => o.exercise_id === exerciseId && o.team_id === n.team_id).length, + ...n, + }))); return (
@@ -170,21 +178,22 @@ const Audiences = () => {
- {sortedAudiences.length > 0 ? ( + {sortedTeams.length > 0 ? ( @@ -217,31 +226,31 @@ const Audiences = () => { primary={
{filtering.buildHeader( - 'audience_name', + 'team_name', 'Name', true, headerStyles, )} {filtering.buildHeader( - 'audience_description', + 'team_description', 'Description', true, headerStyles, )} {filtering.buildHeader( - 'audience_users_number', + 'team_users_number', 'Players', true, headerStyles, )} {filtering.buildHeader( - 'audience_enabled', - 'Status', + 'team_users_enabled_number', + 'Enabled players', true, headerStyles, )} {filtering.buildHeader( - 'audience_tags', + 'team_tags', 'Tags', true, headerStyles, @@ -251,65 +260,57 @@ const Audiences = () => { />   - {sortedAudiences.map((audience) => ( + {sortedTeams.map((team) => ( setSelectedAudience(audience.audience_id)} + onClick={() => setSelectedTeam(team.team_id)} > - +
- {audience.audience_name} + {team.team_name}
- {audience.audience_description} + {team.team_description}
- {audience.audience_users_number} + {team.team_users_number}
- + {team.team_users_enabled_number}
- +
} /> - @@ -317,26 +318,26 @@ const Audiences = () => { ))} setSelectedAudience(null)} + onClose={() => setSelectedTeam(null)} elevation={1} > - {selectedAudience !== null && ( - setSelectedAudience(null)} + handleClose={() => setSelectedTeam(null)} tagsMap={tagsMap} /> )} - {permissions.canWrite && } + {permissions.canWrite && team.team_id)}/>}
); }; -export default Audiences; +export default Teams; diff --git a/openex-front/src/admin/components/exercises/timeline/Timeline.js b/openex-front/src/admin/components/exercises/timeline/Timeline.js index 4eb3c075ca..5b771cb444 100644 --- a/openex-front/src/admin/components/exercises/timeline/Timeline.js +++ b/openex-front/src/admin/components/exercises/timeline/Timeline.js @@ -8,7 +8,7 @@ import * as R from 'ramda'; import { useFormatter } from '../../../../components/i18n'; import { useHelper } from '../../../../store'; import useDataLoader from '../../../../utils/ServerSideEvent'; -import { fetchAudiences } from '../../../../actions/Audience'; +import { fetchExerciseTeams } from '../../../../actions/Exercise'; import { fetchInjects, fetchInjectTypes } from '../../../../actions/Inject'; import Empty from '../../../../components/Empty'; import SearchFilter from '../../../../components/SearchFilter'; @@ -139,49 +139,49 @@ const Timeline = () => { const { t, fndt } = useFormatter(); const { exercise, - audiences, + teams, injects, injectTypesMap, - injectTypesWithNoAudiences, + injectTypesWithNoTeams, exercisesMap, tagsMap, - audiencesInjectsMap, + teamsInjectsMap, technicalInjectsMap, } = useHelper((helper) => { - const exerciseAudiences = helper.getExerciseAudiences(exerciseId); - const injectsPerAudience = R.mergeAll( - exerciseAudiences.map((a) => ({ - [a.audience_id]: helper.getAudienceInjects(a.audience_id), + const exerciseTeams = helper.getExerciseTeams(exerciseId); + const injectsPerTeam = R.mergeAll( + exerciseTeams.map((a) => ({ + [a.team_id]: helper.getTeamInjects(a.team_id), })), ); return { exercise: helper.getExercise(exerciseId), injects: helper.getExerciseInjects(exerciseId), - audiences: exerciseAudiences, + teams: exerciseTeams, exercisesMap: helper.getExercisesMap(), tagsMap: helper.getTagsMap(), - audiencesInjectsMap: injectsPerAudience, + teamsInjectsMap: injectsPerTeam, technicalInjectsMap: helper.getExerciseTechnicalInjectsPerType(exerciseId), injectTypesMap: helper.getInjectTypesMap(), - injectTypesWithNoAudiences: helper.getInjectTypesWithNoAudiences(), + injectTypesWithNoTeams: helper.getInjectTypesWithNoTeams(), }; }); - const technicalAudiences = injectTypesWithNoAudiences + const technicalTeams = injectTypesWithNoTeams .filter( (injectType) => injects.filter((i) => i.inject_type === injectType).length > 0, ) - .map((type) => ({ audience_id: type, audience_name: type })); - const sortedNativeAudiences = R.sortWith( - [R.ascend(R.prop('audience_name'))], - audiences, + .map((type) => ({ team_id: type, team_name: type })); + const sortedNativeTeams = R.sortWith( + [R.ascend(R.prop('team_name'))], + teams, ); - const sortedAudiences = [...technicalAudiences, ...sortedNativeAudiences]; - const injectsMap = { ...audiencesInjectsMap, ...technicalInjectsMap }; + const sortedTeams = [...technicalTeams, ...sortedNativeTeams]; + const injectsMap = { ...teamsInjectsMap, ...technicalInjectsMap }; const [selectedInject, setSelectedInject] = useState(null); useDataLoader(() => { dispatch(fetchInjectTypes()); - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); dispatch(fetchInjects(exerciseId)); }); // Filter and sort hook @@ -252,33 +252,33 @@ const Timeline = () => {
- {sortedAudiences.length > 0 ? ( + {sortedTeams.length > 0 ? (
- {sortedAudiences.map((audience) => ( -
+ {sortedTeams.map((team) => ( +
- {audience.audience_name.startsWith('openex_') ? ( + {team.team_name.startsWith('openex_') ? ( ) : ( )}    - {audience.audience_name.startsWith('openex_') - ? t(audience.audience_name) - : truncate(audience.audience_name, 20)} + {team.team_name.startsWith('openex_') + ? t(team.team_name) + : truncate(team.team_name, 20)}
))}
- {sortedAudiences.map((audience, index) => { + {sortedTeams.map((team, index) => { const injectsGroupedByTick = byTick( - filtering.filterAndSort(injectsMap[audience.audience_id]), + filtering.filterAndSort(injectsMap[team.team_id]), ); return (
@@ -349,7 +349,7 @@ const Timeline = () => {
   - {t('No audience')} + {t('No team')}
diff --git a/openex-front/src/admin/components/exercises/validations/ManualExpectations.tsx b/openex-front/src/admin/components/exercises/validations/ManualExpectations.tsx index 6ddc8a8648..1be76bf1e8 100644 --- a/openex-front/src/admin/components/exercises/validations/ManualExpectations.tsx +++ b/openex-front/src/admin/components/exercises/validations/ManualExpectations.tsx @@ -2,9 +2,9 @@ import React, { FunctionComponent, useState } from 'react'; import { List, ListItemButton, ListItemIcon, ListItemText, Chip } from '@mui/material'; import { AssignmentTurnedIn } from '@mui/icons-material'; import { makeStyles } from '@mui/styles'; -import type { Audience, Inject } from '../../../../utils/api-types'; +import type { Team, Inject } from '../../../../utils/api-types'; import { useHelper } from '../../../../store'; -import type { AudiencesHelper } from '../../../../actions/helper'; +import type { TeamsHelper } from '../../../../actions/helper'; import type { InjectExpectationsStore } from '../injects/expectations/Expectation'; import { useFormatter } from '../../../../components/i18n'; import type { Theme } from '../../../../components/Theme'; @@ -52,18 +52,18 @@ const ManualExpectations: FunctionComponent = ({ const classes = useStyles(); const { t } = useFormatter(); - const { audiencesMap }: { audiencesMap: Record } = useHelper((helper: AudiencesHelper) => { + const { teamsMap }: { teamsMap: Record } = useHelper((helper: TeamsHelper) => { return ({ - audiencesMap: helper.getAudiencesMap(), + teamsMap: helper.getTeamsMap(), }); }); - const groupedByAudience = expectations.reduce((group: Map, expectation) => { - const { inject_expectation_audience } = expectation; - if (inject_expectation_audience) { - const values = group.get(inject_expectation_audience) ?? []; + const groupedByTeam = expectations.reduce((group: Map, expectation) => { + const { inject_expectation_team } = expectation; + if (inject_expectation_team) { + const values = group.get(inject_expectation_team) ?? []; values.push(expectation); - group.set(inject_expectation_audience, values); + group.set(inject_expectation_team, values); } return group; }, new Map()); @@ -73,9 +73,9 @@ const ManualExpectations: FunctionComponent = ({ return ( <> - {Array.from(groupedByAudience) + {Array.from(groupedByTeam) .map(([entry, values]) => { - const audience = audiencesMap[entry] || {}; + const team = teamsMap[entry] || {}; const expectationValues = values .reduce((acc, el) => ({ ...acc, @@ -90,7 +90,7 @@ const ManualExpectations: FunctionComponent = ({ } return ( { injectTypesMap, injectExpectations, injectsMap, - audiencesMap, + teamsMap, challengesMap, articlesMap, mediasMap, @@ -74,7 +73,7 @@ const Validations = () => { injectsMap: helper.getInjectsMap(), injectExpectations: helper.getExerciseInjectExpectations(exerciseId), injectTypesMap: helper.getInjectTypesMap(), - audiencesMap: helper.getAudiencesMap(), + teamsMap: helper.getTeamsMap(), challengesMap: helper.getChallengesMap(), articlesMap: helper.getArticlesMap(), mediasMap: helper.getMediasMap(), @@ -85,7 +84,7 @@ const Validations = () => { dispatch(fetchInjectTypes()); dispatch(fetchExerciseInjectExpectations(exerciseId)); dispatch(fetchExerciseInjects(exerciseId)); - dispatch(fetchAudiences(exerciseId)); + dispatch(fetchExerciseTeams(exerciseId)); dispatch(fetchExerciseArticles(exerciseId)); dispatch(fetchExerciseChallenges(exerciseId)); }); @@ -130,13 +129,13 @@ const Validations = () => { return group; }, {}); - const groupedByAudience = (injects) => { + const groupedByTeam = (injects) => { return injects.reduce((group, expectation) => { - const { inject_expectation_audience } = expectation; - if (inject_expectation_audience) { - const values = group[inject_expectation_audience] ?? []; + const { inject_expectation_team } = expectation; + if (inject_expectation_team) { + const values = group[inject_expectation_team] ?? []; values.push(expectation); - group[inject_expectation_audience] = values; + group[inject_expectation_team] = values; } return group; }, {}); @@ -186,7 +185,7 @@ const Validations = () => {
- {Object.entries(groupedByInject).map(([injectId, audiences]) => { + {Object.entries(groupedByInject).map(([injectId, teams]) => { const inject = injectsMap[injectId] || {}; const injectContract = injectTypesMap[inject.inject_contract] || {}; return ( @@ -218,10 +217,10 @@ const Validations = () => { /> - {Object.entries(groupedByAudience(audiences)).map(([audienceId, expectations]) => { - const audience = audiencesMap[audienceId] || {}; + {Object.entries(groupedByTeam(teams)).map(([teamId, expectations]) => { + const team = teamsMap[teamId] || {}; return ( -
+
{ - {audience.audience_name} + {team.team_name}
} /> diff --git a/openex-front/src/admin/components/exercises/variables/Variables.tsx b/openex-front/src/admin/components/exercises/variables/Variables.tsx index 7e5391b6b9..f82c81f809 100644 --- a/openex-front/src/admin/components/exercises/variables/Variables.tsx +++ b/openex-front/src/admin/components/exercises/variables/Variables.tsx @@ -117,9 +117,7 @@ const Variables = () => { useDataLoader(() => { dispatch(fetchVariables(exerciseId)); }); - const permissions = usePermissions(exerciseId); - const sortedVariables: [Variable] = filtering.filterAndSort(variables); return (
diff --git a/openex-front/src/admin/components/integrations/Integrations.js b/openex-front/src/admin/components/integrations/Integrations.js index 953d3a12a9..d7c2946809 100644 --- a/openex-front/src/admin/components/integrations/Integrations.js +++ b/openex-front/src/admin/components/integrations/Integrations.js @@ -47,7 +47,7 @@ const iconField = (type) => { return ; case 'attachment': return ; - case 'audience': + case 'team': return ; case 'select': case 'dependency-select': diff --git a/openex-front/src/admin/components/nav/LeftBar.tsx b/openex-front/src/admin/components/nav/LeftBar.tsx index d17499b551..de7261a0be 100644 --- a/openex-front/src/admin/components/nav/LeftBar.tsx +++ b/openex-front/src/admin/components/nav/LeftBar.tsx @@ -3,16 +3,18 @@ import { Link, useLocation } from 'react-router-dom'; import { Drawer, ListItemText, Toolbar, MenuList, MenuItem, ListItemIcon, Divider, Tooltip, tooltipClasses } from '@mui/material'; import { DashboardOutlined, - RowingOutlined, - GroupsOutlined, - DescriptionOutlined, + PolicyOutlined, + MovieFilterOutlined, + HubOutlined, ExtensionOutlined, SettingsOutlined, DomainOutlined, + Groups3Outlined, EmojiEventsOutlined, - SchoolOutlined, ChevronLeft, ChevronRight, + DnsOutlined, + DescriptionOutlined, } from '@mui/icons-material'; import { NewspaperVariantMultipleOutline } from 'mdi-material-ui'; import { createStyles, makeStyles, styled, useTheme } from '@mui/styles'; @@ -151,7 +153,10 @@ const LeftBar = () => { )} - + + + + { classes={{ root: classes.menuItem }} > - + {navOpen && ( )} - + - + {navOpen && ( - + )} - + - + + {navOpen && ( + )} - + + {navOpen && ( + + )} + + + + + + + + + + + + + + + + {navOpen && ( + + )} + + { - + diff --git a/openex-front/src/admin/components/nav/TopBar.tsx b/openex-front/src/admin/components/nav/TopBar.tsx index 03ee20415d..2899c6030f 100644 --- a/openex-front/src/admin/components/nav/TopBar.tsx +++ b/openex-front/src/admin/components/nav/TopBar.tsx @@ -9,7 +9,7 @@ import TopMenuDashboard from './TopMenuDashboard'; import TopMenuSettings from './TopMenuSettings'; import TopMenuExercises from './TopMenuExercises'; import TopMenuExercise from './TopMenuExercise'; -import TopMenuPlayers from './TopMenuPlayers'; +import TopMenuPersons from './TopMenuPersons'; import TopMenuOrganizations from './TopMenuOrganizations'; import TopMenuDocuments from './TopMenuDocuments'; import TopMenuMedias from '../medias/TopMenuMedias'; @@ -117,7 +117,7 @@ const TopBar: React.FC = () => { {location.pathname.includes('/admin/exercises/') && ( )} - {location.pathname.includes('/admin/players') && } + {location.pathname.includes('/admin/persons') && } {location.pathname.includes('/admin/organizations') && ( )} diff --git a/openex-front/src/admin/components/nav/TopMenuExercise.tsx b/openex-front/src/admin/components/nav/TopMenuExercise.tsx index 6ed19792d5..38c5c9e918 100644 --- a/openex-front/src/admin/components/nav/TopMenuExercise.tsx +++ b/openex-front/src/admin/components/nav/TopMenuExercise.tsx @@ -74,7 +74,7 @@ const TopMenuExercise: React.FC = () => { -
+ + ); }; -export default TopMenuPlayers; +export default TopMenuPersons; diff --git a/openex-front/src/admin/components/persons/Index.tsx b/openex-front/src/admin/components/persons/Index.tsx new file mode 100644 index 0000000000..81ce93eda5 --- /dev/null +++ b/openex-front/src/admin/components/persons/Index.tsx @@ -0,0 +1,31 @@ +import React, { Suspense, lazy } from 'react'; +import { makeStyles } from '@mui/styles'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { errorWrapper } from '../../../components/Error'; +import Loader from '../../../components/Loader'; + +const Players = lazy(() => import('./Players')); +const Teams = lazy(() => import('./Teams')); + +const useStyles = makeStyles(() => ({ + root: { + flexGrow: 1, + }, +})); + +const Index = () => { + const classes = useStyles(); + return ( +
+ }> + + } /> + + + + +
+ ); +}; + +export default Index; diff --git a/openex-front/src/admin/components/players/Players.js b/openex-front/src/admin/components/persons/Players.js similarity index 96% rename from openex-front/src/admin/components/players/Players.js rename to openex-front/src/admin/components/persons/Players.js index 6308d05b44..a9ca38ba8f 100644 --- a/openex-front/src/admin/components/players/Players.js +++ b/openex-front/src/admin/components/persons/Players.js @@ -7,8 +7,8 @@ import { CSVLink } from 'react-csv'; import { fetchPlayers } from '../../../actions/User'; import { fetchOrganizations } from '../../../actions/Organization'; import ItemTags from '../../../components/ItemTags'; -import CreatePlayer from './CreatePlayer'; -import PlayerPopover from './PlayerPopover'; +import CreatePlayer from './players/CreatePlayer'; +import PlayerPopover from './players/PlayerPopover'; import TagsFilter from '../../../components/TagsFilter'; import SearchFilter from '../../../components/SearchFilter'; import { fetchTags } from '../../../actions/Tag'; @@ -137,7 +137,8 @@ const Players = () => { ]; const filtering = useSearchAnFilter('user', 'email', searchColumns); // Fetching data - const { users, organizationsMap, tagsMap } = useHelper((helper) => ({ + const { isPlanner, users, organizationsMap, tagsMap } = useHelper((helper) => ({ + isPlanner: helper.getMe().user_is_planner, users: helper.getUsers(), organizationsMap: helper.getOrganizationsMap(), tagsMap: helper.getTagsMap(), @@ -149,7 +150,7 @@ const Players = () => { }); const sortedUsers = filtering.filterAndSort(users); return ( -
+ <>
{ ))} - -
+ {isPlanner && } + ); }; diff --git a/openex-front/src/admin/components/persons/Teams.js b/openex-front/src/admin/components/persons/Teams.js new file mode 100644 index 0000000000..86fb699762 --- /dev/null +++ b/openex-front/src/admin/components/persons/Teams.js @@ -0,0 +1,327 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@mui/styles'; +import { List, ListItem, ListItemIcon, ListItemText, ListItemSecondaryAction, Tooltip, IconButton, Drawer } from '@mui/material'; +import { useDispatch } from 'react-redux'; +import { FileDownloadOutlined, GroupsOutlined } from '@mui/icons-material'; +import { CSVLink } from 'react-csv'; +import { fetchTeams } from '../../../actions/Team'; +import { fetchOrganizations } from '../../../actions/Organization'; +import ItemTags from '../../../components/ItemTags'; +import CreateTeam from './teams/CreateTeam'; +import TeamPopover from './teams/TeamPopover'; +import TagsFilter from '../../../components/TagsFilter'; +import SearchFilter from '../../../components/SearchFilter'; +import { fetchTags } from '../../../actions/Tag'; +import useDataLoader from '../../../utils/ServerSideEvent'; +import { useHelper } from '../../../store'; +import useSearchAnFilter from '../../../utils/SortingFiltering'; +import { useFormatter } from '../../../components/i18n'; +import { exportData } from '../../../utils/Environment'; +import TeamPlayers from './teams/TeamPlayers'; + +const useStyles = makeStyles(() => ({ + parameters: { + marginTop: -10, + }, + container: { + marginTop: 10, + }, + itemHead: { + paddingLeft: 10, + textTransform: 'uppercase', + cursor: 'pointer', + }, + item: { + paddingLeft: 10, + height: 50, + }, + bodyItem: { + height: '100%', + fontSize: 13, + }, + drawerPaper: { + minHeight: '100vh', + width: '50%', + padding: 0, + }, +})); + +const headerStyles = { + iconSort: { + position: 'absolute', + margin: '0 0 0 5px', + padding: 0, + top: '0px', + }, + team_name: { + float: 'left', + width: '20%', + fontSize: 12, + fontWeight: '700', + }, + team_description: { + float: 'left', + width: '25%', + fontSize: 12, + fontWeight: '700', + }, + team_users_number: { + float: 'left', + width: '8%', + fontSize: 12, + fontWeight: '700', + }, + team_organization: { + float: 'left', + width: '20%', + fontSize: 12, + fontWeight: '700', + }, + team_tags: { + float: 'left', + width: '25%', + fontSize: 12, + fontWeight: '700', + }, +}; + +const inlineStyles = { + team_name: { + float: 'left', + width: '20%', + height: 20, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + team_description: { + float: 'left', + width: '25%', + height: 20, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + team_users_number: { + float: 'left', + width: '8%', + height: 20, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + team_organization: { + float: 'left', + width: '20%', + height: 20, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + team_tags: { + float: 'left', + width: '25%', + height: 20, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, +}; + +const Teams = () => { + // Standard hooks + const classes = useStyles(); + const dispatch = useDispatch(); + const { t } = useFormatter(); + const [selectedTeam, setSelectedTeam] = useState(null); + // Filter and sort hook + const searchColumns = [ + 'name', + 'description', + 'organization', + ]; + const filtering = useSearchAnFilter('team', 'name', searchColumns); + // Fetching data + const { isPlanner, teams, organizationsMap, tagsMap } = useHelper((helper) => ({ + isPlanner: helper.getMe().user_is_planner, + teams: helper.getTeams(), + organizationsMap: helper.getOrganizationsMap(), + tagsMap: helper.getTagsMap(), + })); + useDataLoader(() => { + dispatch(fetchTags()); + dispatch(fetchOrganizations()); + dispatch(fetchTeams()); + }); + const sortedTeams = filtering.filterAndSort(teams); + return ( + <> +
+
+ +
+
+ +
+
+ {sortedTeams.length > 0 ? ( + + + + + + + + ) : ( + + + + )} +
+
+
+ + + + +   + + + + {filtering.buildHeader( + 'team_name', + 'Name', + true, + headerStyles, + )} + {filtering.buildHeader( + 'team_description', + 'Description', + true, + headerStyles, + )} + {filtering.buildHeader('team_users_number', 'Players', true, headerStyles)} + {filtering.buildHeader( + 'team_organization', + 'Organization', + true, + headerStyles, + )} + {filtering.buildHeader('team_tags', 'Tags', true, headerStyles)} +
+ } + /> +   + + {sortedTeams.map((team) => ( + setSelectedTeam(team.team_id)} + > + + + + +
+ {team.team_name} +
+
+ {team.team_description} +
+
+ {team.team_users_number} +
+
+ {organizationsMap[team.team_organization] + ?.organization_name || '-'} +
+
+ +
+
+ } + /> + + + + + ))} + + setSelectedTeam(null)} + elevation={1} + > + {selectedTeam !== null && ( + setSelectedTeam(null)} + tagsMap={tagsMap} + /> + )} + + {isPlanner && } + + ); +}; + +export default Teams; diff --git a/openex-front/src/admin/components/players/CreatePlayer.tsx b/openex-front/src/admin/components/persons/players/CreatePlayer.tsx similarity index 86% rename from openex-front/src/admin/components/players/CreatePlayer.tsx rename to openex-front/src/admin/components/persons/players/CreatePlayer.tsx index 61572ba74e..de4fc077e7 100644 --- a/openex-front/src/admin/components/players/CreatePlayer.tsx +++ b/openex-front/src/admin/components/persons/players/CreatePlayer.tsx @@ -2,14 +2,14 @@ import React, { FunctionComponent, useState } from 'react'; import { Fab, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import { Add, ControlPointOutlined } from '@mui/icons-material'; import { makeStyles } from '@mui/styles'; -import { addPlayer } from '../../../actions/User'; +import { addPlayer } from '../../../../actions/User'; import PlayerForm from './PlayerForm'; -import { useFormatter } from '../../../components/i18n'; -import Dialog from '../../../components/common/Dialog'; -import { useAppDispatch } from '../../../utils/hooks'; -import type { Theme } from '../../../components/Theme'; -import type { CreatePlayerInput } from '../../../utils/api-types'; -import { Option } from '../../../utils/Option'; +import { useFormatter } from '../../../../components/i18n'; +import Dialog from '../../../../components/common/Dialog'; +import { useAppDispatch } from '../../../../utils/hooks'; +import type { Theme } from '../../../../components/Theme'; +import type { CreatePlayerInput } from '../../../../utils/api-types'; +import { Option } from '../../../../utils/Option'; import type { PlayerInputForm } from './Player'; const useStyles = makeStyles((theme: Theme) => ({ diff --git a/openex-front/src/admin/components/players/Player.d.ts b/openex-front/src/admin/components/persons/players/Player.d.ts similarity index 74% rename from openex-front/src/admin/components/players/Player.d.ts rename to openex-front/src/admin/components/persons/players/Player.d.ts index 545d98be89..62a5b33403 100644 --- a/openex-front/src/admin/components/players/Player.d.ts +++ b/openex-front/src/admin/components/persons/players/Player.d.ts @@ -1,5 +1,5 @@ -import type { UpdatePlayerInput, User } from '../../../utils/api-types'; -import { Option } from '../../../utils/Option'; +import type { UpdatePlayerInput, User } from '../../../../utils/api-types'; +import { Option } from '../../../../utils/Option'; export type PlayerInputForm = Omit< UpdatePlayerInput, diff --git a/openex-front/src/admin/components/players/PlayerForm.tsx b/openex-front/src/admin/components/persons/players/PlayerForm.tsx similarity index 90% rename from openex-front/src/admin/components/players/PlayerForm.tsx rename to openex-front/src/admin/components/persons/players/PlayerForm.tsx index 222661d052..270bcd449a 100644 --- a/openex-front/src/admin/components/players/PlayerForm.tsx +++ b/openex-front/src/admin/components/persons/players/PlayerForm.tsx @@ -3,14 +3,14 @@ import { Form } from 'react-final-form'; import { Button } from '@mui/material'; import { makeStyles } from '@mui/styles'; import { z } from 'zod'; -import TextField from '../../../components/TextField'; -import { useFormatter } from '../../../components/i18n'; -import TagField from '../../../components/TagField'; -import OrganizationField from '../../../components/OrganizationField'; -import CountryField from '../../../components/CountryField'; -import type { Theme } from '../../../components/Theme'; +import TextField from '../../../../components/TextField'; +import { useFormatter } from '../../../../components/i18n'; +import TagField from '../../../../components/TagField'; +import OrganizationField from '../../../../components/OrganizationField'; +import CountryField from '../../../../components/CountryField'; +import type { Theme } from '../../../../components/Theme'; import type { PlayerInputForm } from './Player'; -import { schemaValidator } from '../../../utils/Zod'; +import { schemaValidator } from '../../../../utils/Zod'; const useStyles = makeStyles((theme: Theme) => ({ container: { @@ -37,7 +37,6 @@ const PlayerForm: FunctionComponent = ({ }) => { const classes = useStyles(); const { t } = useFormatter(); - const playerFormSchemaValidation = z.object({ user_email: z.string().email(t('Should be a valid email address')), user_phone: z @@ -49,7 +48,6 @@ const PlayerForm: FunctionComponent = ({ .optional() .nullable(), }); - return ( = ({ user, - exerciseId, - audienceId, - audienceUsersIds, + teamId, + teamUsersIds, }) => { const { t } = useFormatter(); const dispatch = useAppDispatch(); - const { userAdmin, exercise, organizationsMap, tagsMap } = useHelper( + const { userAdmin, organizationsMap, tagsMap } = useHelper( ( helper: ExercicesHelper & UsersHelper & OrganizationsHelper & TagsHelper, ) => { return { userAdmin: helper.getMe()?.user_admin, - exercise: helper.getExercise(exerciseId), organizationsMap: helper.getOrganizationsMap(), tagsMap: helper.getTagsMap(), }; @@ -96,8 +93,8 @@ const PlayerPopover: FunctionComponent = ({ const submitRemove = () => { return dispatch( - updateAudiencePlayers(exerciseId, audienceId, { - audience_users: audienceUsersIds?.filter((id) => id !== user.user_id), + updateTeamPlayers(teamId, { + team_users: teamUsersIds?.filter((id) => id !== user.user_id), }), ).then(() => handleCloseRemove()); }; @@ -124,12 +121,9 @@ const PlayerPopover: FunctionComponent = ({ onClose={handlePopoverClose} > {t('Update')} - {audienceId && ( - - {t('Remove from the audience')} + {teamId && ( + + {t('Remove from the team')} )} {canDelete && ( @@ -175,7 +169,7 @@ const PlayerPopover: FunctionComponent = ({ > - {t('Do you want to remove the player from the audience?')} + {t('Do you want to remove the player from the team?')} diff --git a/openex-front/src/admin/components/persons/teams/CreateTeam.tsx b/openex-front/src/admin/components/persons/teams/CreateTeam.tsx new file mode 100644 index 0000000000..477b79b6f1 --- /dev/null +++ b/openex-front/src/admin/components/persons/teams/CreateTeam.tsx @@ -0,0 +1,102 @@ +import React, { FunctionComponent, useState } from 'react'; +import { Fab, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; +import { Add, ControlPointOutlined } from '@mui/icons-material'; +import { makeStyles } from '@mui/styles'; +import { addTeam } from '../../../../actions/Team'; +import TeamForm from './TeamForm'; +import { useFormatter } from '../../../../components/i18n'; +import Dialog from '../../../../components/common/Dialog'; +import { useAppDispatch } from '../../../../utils/hooks'; +import type { Theme } from '../../../../components/Theme'; +import type { TeamCreateInput } from '../../../../utils/api-types'; +import { Option } from '../../../../utils/Option'; +import type { TeamInputForm } from './Team'; + +const useStyles = makeStyles((theme: Theme) => ({ + createButton: { + position: 'fixed', + bottom: 30, + right: 30, + }, + text: { + fontSize: theme.typography.h2.fontSize, + color: theme.palette.primary.main, + fontWeight: theme.typography.h2.fontWeight, + }, +})); + +interface CreateTeamProps { + inline: boolean; + onCreate: (result: string) => void; +} + +const CreateTeam: FunctionComponent = ({ + inline, + onCreate, +}) => { + const classes = useStyles(); + const { t } = useFormatter(); + const dispatch = useAppDispatch(); + + const [openDialog, setOpenDialog] = useState(false); + + const handleOpen = () => setOpenDialog(true); + const handleClose = () => setOpenDialog(false); + + const onSubmit = (data: TeamInputForm) => { + const inputValues: TeamCreateInput = { + ...data, + team_organization: data.team_organization?.id, + team_tags: data.team_tags?.map((tag: Option) => tag.id), + }; + return dispatch(addTeam(inputValues)).then( + (result: { result: string }) => { + if (result.result) { + if (onCreate) { + onCreate(result.result); + } + return handleClose(); + } + return result; + }, + ); + }; + + return ( +
+ {inline ? ( + + + + + + + ) : ( + + + + )} + + + +
+ ); +}; + +export default CreateTeam; diff --git a/openex-front/src/admin/components/persons/teams/Team.d.ts b/openex-front/src/admin/components/persons/teams/Team.d.ts new file mode 100644 index 0000000000..5e3290f509 --- /dev/null +++ b/openex-front/src/admin/components/persons/teams/Team.d.ts @@ -0,0 +1,14 @@ +import type { TeamUpdateInput, Team } from '../../../../utils/api-types'; +import { Option } from '../../../../utils/Option'; + +export type TeamInputForm = Omit< +TeamUpdateInput, +'team_organization' | 'team_tags' +> & { + team_organization: Option | undefined; + team_tags: Option[]; +}; +export type TeamStore = Omit & { + team_organization: string | undefined; + team_tags: string[] | undefined; +}; diff --git a/openex-front/src/admin/components/exercises/audiences/AudienceAddPlayers.js b/openex-front/src/admin/components/persons/teams/TeamAddPlayers.js similarity index 83% rename from openex-front/src/admin/components/exercises/audiences/AudienceAddPlayers.js rename to openex-front/src/admin/components/persons/teams/TeamAddPlayers.js index 5a18b81ec0..ceb087ab77 100644 --- a/openex-front/src/admin/components/exercises/audiences/AudienceAddPlayers.js +++ b/openex-front/src/admin/components/persons/teams/TeamAddPlayers.js @@ -5,14 +5,13 @@ import * as R from 'ramda'; import { Button, Slide, Chip, Avatar, List, ListItem, ListItemText, Dialog, DialogTitle, DialogContent, DialogActions, Box, ListItemIcon, Grid, Fab } from '@mui/material'; import { Add, PersonOutlined } from '@mui/icons-material'; import withStyles from '@mui/styles/withStyles'; -import { updateAudiencePlayers } from '../../../../actions/Audience'; +import { updateTeamPlayers } from '../../../../actions/Team'; import SearchFilter from '../../../../components/SearchFilter'; import inject18n from '../../../../components/i18n'; import { storeHelper } from '../../../../actions/Schema'; import { fetchPlayers } from '../../../../actions/User'; -import CreatePlayer from '../../players/CreatePlayer'; +import CreatePlayer from '../players/CreatePlayer'; import { resolveUserName, truncate } from '../../../../utils/String'; -import { isExerciseReadOnly } from '../../../../utils/Exercise'; import ItemTags from '../../../../components/ItemTags'; import TagsFilter from '../../../../components/TagsFilter'; @@ -38,7 +37,7 @@ const Transition = React.forwardRef((props, ref) => ( )); Transition.displayName = 'TransitionSlide'; -class AudienceAddPlayers extends Component { +class TeamAddPlayers extends Component { constructor(props) { super(props); this.state = { @@ -86,12 +85,11 @@ class AudienceAddPlayers extends Component { } submitAddUsers() { - this.props.updateAudiencePlayers( - this.props.exerciseId, - this.props.audienceId, + this.props.updateTeamPlayers( + this.props.teamId, { - audience_users: R.uniq([ - ...this.props.audienceUsersIds, + team_users: R.uniq([ + ...this.props.teamUsersIds, ...this.state.usersIds, ]), }, @@ -108,26 +106,17 @@ class AudienceAddPlayers extends Component { classes, t, usersMap, - audienceUsersIds, - exercise, + teamUsersIds, organizationsMap, } = this.props; const { keyword, usersIds, tags } = this.state; const filterByKeyword = (n) => keyword === '' - || (n.user_email || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_firstname || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_lastname || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_phone || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.organization_name || '') - .toLowerCase() - .indexOf(keyword.toLowerCase()) !== -1 - || (n.organization_description || '') - .toLowerCase() - .indexOf(keyword.toLowerCase()) !== -1; + || (n.user_email || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_firstname || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_lastname || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_phone || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.organization_name || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.organization_description || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1; const filteredUsers = R.pipe( R.map((u) => ({ organization_name: @@ -154,7 +143,6 @@ class AudienceAddPlayers extends Component { color="primary" aria-label="Add" className={classes.createButton} - disabled={isExerciseReadOnly(exercise)} > @@ -172,7 +160,7 @@ class AudienceAddPlayers extends Component { }, }} > - {t('Add players in this audience')} + {t('Add players in this team')} @@ -195,7 +183,7 @@ class AudienceAddPlayers extends Component { {filteredUsers.map((user) => { const disabled = usersIds.includes(user.user_id) - || audienceUsersIds.includes(user.user_id); + || teamUsersIds.includes(user.user_id); return ( { +const select = (state) => { const helper = storeHelper(state); - const { exerciseId } = ownProps; return { - exercise: helper.getExercise(exerciseId), usersMap: helper.getUsersMap(), organizationsMap: helper.getOrganizationsMap(), }; }; export default R.compose( - connect(select, { updateAudiencePlayers, fetchPlayers }), + connect(select, { updateTeamPlayers, fetchPlayers }), inject18n, withStyles(styles), -)(AudienceAddPlayers); +)(TeamAddPlayers); diff --git a/openex-front/src/admin/components/persons/teams/TeamForm.tsx b/openex-front/src/admin/components/persons/teams/TeamForm.tsx new file mode 100644 index 0000000000..187cfe3f0f --- /dev/null +++ b/openex-front/src/admin/components/persons/teams/TeamForm.tsx @@ -0,0 +1,97 @@ +import React, { FunctionComponent } from 'react'; +import { Form } from 'react-final-form'; +import { Button } from '@mui/material'; +import { makeStyles } from '@mui/styles'; +import { z } from 'zod'; +import TextField from '../../../../components/TextField'; +import { useFormatter } from '../../../../components/i18n'; +import TagField from '../../../../components/TagField'; +import OrganizationField from '../../../../components/OrganizationField'; +import type { Theme } from '../../../../components/Theme'; +import type { TeamInputForm } from './Team'; +import { schemaValidator } from '../../../../utils/Zod'; + +const useStyles = makeStyles((theme: Theme) => ({ + container: { + display: 'flex', + gap: theme.spacing(2), + placeContent: 'end', + }, +})); + +interface TeamFormProps { + initialValues: Partial; + handleClose: () => void; + onSubmit: (data: TeamInputForm) => void; + editing?: boolean; +} + +const TeamForm: FunctionComponent = ({ + editing, + onSubmit, + initialValues, + handleClose, +}) => { + const classes = useStyles(); + const { t } = useFormatter(); + const teamFormSchemaValidation = z.object({ + team_name: z.string().min(2, t('This field is mandatory')), + }); + return ( + { + changeValue(state, field, () => value); + }, + }} + > + {({ handleSubmit, form, values, submitting, pristine }) => ( + + + + + +
+ + +
+ + )} + + ); +}; + +export default TeamForm; diff --git a/openex-front/src/admin/components/exercises/audiences/AudiencePlayers.js b/openex-front/src/admin/components/persons/teams/TeamPlayers.js similarity index 74% rename from openex-front/src/admin/components/exercises/audiences/AudiencePlayers.js rename to openex-front/src/admin/components/persons/teams/TeamPlayers.js index df09e3e339..611cede00c 100644 --- a/openex-front/src/admin/components/exercises/audiences/AudiencePlayers.js +++ b/openex-front/src/admin/components/persons/teams/TeamPlayers.js @@ -6,14 +6,17 @@ import { List, ListItem, ListItemIcon, ListItemText, ListItemSecondaryAction, Ic import { connect } from 'react-redux'; import { ArrowDropDownOutlined, ArrowDropUpOutlined, CloseRounded, EmailOutlined, KeyOutlined, PersonOutlined, SmartphoneOutlined } from '@mui/icons-material'; import inject18n from '../../../../components/i18n'; -import { fetchAudiencePlayers } from '../../../../actions/Audience'; +import { fetchTeamPlayers } from '../../../../actions/Team'; import { fetchOrganizations } from '../../../../actions/Organization'; +import { addExerciseTeamPlayers, removeExerciseTeamPlayers } from '../../../../actions/Exercise'; import SearchFilter from '../../../../components/SearchFilter'; import TagsFilter from '../../../../components/TagsFilter'; import ItemTags from '../../../../components/ItemTags'; -import PlayerPopover from '../../players/PlayerPopover'; +import PlayerPopover from '../players/PlayerPopover'; import { storeHelper } from '../../../../actions/Schema'; -import AudienceAddPlayers from './AudienceAddPlayers'; +import TeamAddPlayers from './TeamAddPlayers'; +import ItemBoolean from '../../../../components/ItemBoolean'; +import { isExerciseUpdatable } from '../../../../utils/Exercise'; const styles = (theme) => ({ header: { @@ -77,6 +80,12 @@ const inlineStylesHeaders = { padding: 0, top: '0px', }, + user_enabled: { + float: 'left', + width: '12%', + fontSize: 12, + fontWeight: '700', + }, user_email: { float: 'left', width: '30%', @@ -91,19 +100,27 @@ const inlineStylesHeaders = { }, user_organization: { float: 'left', - width: '25%', + width: '20%', fontSize: 12, fontWeight: '700', }, user_tags: { float: 'left', - width: '30%', + width: '23%', fontSize: 12, fontWeight: '700', }, }; const inlineStyles = { + user_enabled: { + float: 'left', + width: '12%', + height: 20, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, user_email: { float: 'left', width: '30%', @@ -122,7 +139,7 @@ const inlineStyles = { }, user_organization: { float: 'left', - width: '25%', + width: '20%', height: 20, whiteSpace: 'nowrap', overflow: 'hidden', @@ -130,7 +147,7 @@ const inlineStyles = { }, user_tags: { float: 'left', - width: '30%', + width: '23%', height: 20, whiteSpace: 'nowrap', overflow: 'hidden', @@ -138,7 +155,7 @@ const inlineStyles = { }, }; -class AudiencesPlayers extends Component { +class TeamsPlayers extends Component { constructor(props) { super(props); this.state = { @@ -150,9 +167,9 @@ class AudiencesPlayers extends Component { } componentDidMount() { - const { exerciseId, audienceId } = this.props; + const { teamId } = this.props; this.props.fetchOrganizations(); - this.props.fetchAudiencePlayers(exerciseId, audienceId); + this.props.fetchTeamPlayers(teamId); } handleSearch(value) { @@ -199,29 +216,47 @@ class AudiencesPlayers extends Component { ); } + handleToggleUser(userId, enabled) { + if (!enabled) { + this.props.addExerciseTeamPlayers( + this.props.exerciseId, + this.props.teamId, + { + exercise_team_players: [userId], + }, + ); + } else { + this.props.removeExerciseTeamPlayers( + this.props.exerciseId, + this.props.teamId, + { + exercise_team_players: [userId], + }, + ); + } + } + render() { const { classes, users, handleClose, - audience, + team, organizationsMap, exerciseId, - audienceId, + exercise, + teamId, + t, + isPlanner, } = this.props; + const isWritePermission = exercise ? isExerciseUpdatable(exercise) : isPlanner; const { keyword, sortBy, orderAsc, tags } = this.state; const filterByKeyword = (n) => keyword === '' - || (n.user_email || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_firstname || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_lastname || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_phone || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.user_organization || '') - .toLowerCase() - .indexOf(keyword.toLowerCase()) !== -1; + || (n.user_email || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_firstname || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_lastname || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_phone || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1 + || (n.user_organization || '').toLowerCase().indexOf(keyword.toLowerCase()) !== -1; const sort = R.sortWith( orderAsc ? [R.ascend(R.prop(sortBy))] : [R.descend(R.prop(sortBy))], ); @@ -234,10 +269,14 @@ class AudiencesPlayers extends Component { ), ), R.filter(filterByKeyword), + R.map((n) => ({ + user_enabled: exercise && exercise.exercise_teams_users.filter((o) => o.exercise_id === exerciseId && o.team_id === teamId && n.user_id === o.user_id).length > 0, + ...n, + })), sort, )(users); return ( -
+ <>
- {R.propOr('-', 'audience_name', audience)} + {R.propOr('-', 'team_name', team)}
@@ -289,12 +328,13 @@ class AudiencesPlayers extends Component { + <> + {exerciseId && this.sortHeader('user_enabled', 'Enabled', true)} {this.sortHeader('user_email', 'Email address', true)} {this.sortHeader('user_options', 'Options', false)} {this.sortHeader('user_organization', 'Organization', true)} {this.sortHeader('user_tags', 'Tags', true)} -
+ } />   @@ -310,7 +350,17 @@ class AudiencesPlayers extends Component { + <> + {exerciseId && ( +
+ +
+ )}
-
+ } /> - u.user_id)} - /> + {isWritePermission + ? ( u.user_id)} + />) + :   + } ))} - u.user_id)} - /> -
+ {isWritePermission + && ( u.user_id)} + />) + } + ); } } -AudiencesPlayers.propTypes = { +TeamsPlayers.propTypes = { t: PropTypes.func, nsdt: PropTypes.func, + teamId: PropTypes.string, + team: PropTypes.object, + isPlanner: PropTypes.bool, exerciseId: PropTypes.string, - audienceId: PropTypes.string, - audience: PropTypes.object, + exercise: PropTypes.object, organizations: PropTypes.array, users: PropTypes.array, - fetchAudiencePlayers: PropTypes.func, + fetchTeamPlayers: PropTypes.func, fetchOrganizations: PropTypes.func, + addExerciseTeamPlayers: PropTypes.func, + removeExerciseTeamPlayers: PropTypes.func, handleClose: PropTypes.func, }; const select = (state, ownProps) => { const helper = storeHelper(state); - const { audienceId } = ownProps; + const { teamId, exerciseId } = ownProps; return { + isPlanner: helper.getMe().user_is_planner, organizationsMap: helper.getOrganizationsMap(), - audience: helper.getAudience(audienceId), - users: helper.getAudienceUsers(audienceId), + exercise: exerciseId && helper.getExercise(exerciseId), + team: helper.getTeam(teamId), + users: helper.getTeamUsers(teamId), }; }; export default R.compose( - connect(select, { fetchAudiencePlayers, fetchOrganizations }), + connect(select, { fetchTeamPlayers, fetchOrganizations, addExerciseTeamPlayers, removeExerciseTeamPlayers }), inject18n, withStyles(styles), -)(AudiencesPlayers); +)(TeamsPlayers); diff --git a/openex-front/src/admin/components/persons/teams/TeamPopover.tsx b/openex-front/src/admin/components/persons/teams/TeamPopover.tsx new file mode 100644 index 0000000000..adf3321777 --- /dev/null +++ b/openex-front/src/admin/components/persons/teams/TeamPopover.tsx @@ -0,0 +1,182 @@ +import React, { FunctionComponent, useState } from 'react'; +import { Dialog as MuiDialog, DialogContent, DialogContentText, DialogActions, Button, IconButton, Menu, MenuItem } from '@mui/material'; +import { MoreVert } from '@mui/icons-material'; +import Dialog from '../../../../components/common/Dialog'; +import { deleteTeam, updateTeam } from '../../../../actions/Team'; +import { removeExerciseTeams } from '../../../../actions/Exercise'; +import TeamForm from './TeamForm'; +import { useFormatter } from '../../../../components/i18n'; +import { useAppDispatch } from '../../../../utils/hooks'; +import Transition from '../../../../components/common/Transition'; +import type { TeamUpdateInput } from '../../../../utils/api-types'; +import { Option, organizationOption, tagOptions } from '../../../../utils/Option'; +import { useHelper } from '../../../../store'; +import type { ExercicesHelper, OrganizationsHelper, TagsHelper, TeamsHelper } from '../../../../actions/helper'; +import type { TeamInputForm, TeamStore } from './Team'; + +interface TeamPopoverProps { + team: TeamStore; + exerciseId?: string; + onRemoveTeam?: (teamId: string | undefined) => void, +} + +const TeamPopover: FunctionComponent = ({ + team, + exerciseId, + onRemoveTeam, +}) => { + const { t } = useFormatter(); + const dispatch = useAppDispatch(); + const { organizationsMap, tagsMap } = useHelper( + ( + helper: ExercicesHelper & TeamsHelper & OrganizationsHelper & TagsHelper, + ) => { + return { + organizationsMap: helper.getOrganizationsMap(), + tagsMap: helper.getTagsMap(), + }; + }, + ); + + const [openDelete, setOpenDelete] = useState(false); + const [openEdit, setOpenEdit] = useState(false); + const [openRemove, setOpenRemove] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + + // Popover + const handlePopoverOpen = (event: React.MouseEvent) => { + event.stopPropagation(); + setAnchorEl(event.currentTarget); + }; + + const handlePopoverClose = () => setAnchorEl(null); + + // Edition + const handleOpenEdit = () => { + setOpenEdit(true); + handlePopoverClose(); + }; + + const handleCloseEdit = () => setOpenEdit(false); + + const onSubmitEdit = (data: TeamInputForm) => { + const inputValues: TeamUpdateInput = { + ...data, + team_organization: data.team_organization?.id, + team_tags: data.team_tags?.map((tag: Option) => tag.id), + }; + return dispatch(updateTeam(team.team_id, inputValues)).then(() => handleCloseEdit()); + }; + + // Deletion + const handleOpenDelete = () => { + setOpenDelete(true); + handlePopoverClose(); + }; + + const handleCloseDelete = () => setOpenDelete(false); + + const submitDelete = () => { + dispatch(deleteTeam(team.team_id)).then(() => handleCloseDelete()); + }; + + // Remove + const handleOpenRemove = () => { + setOpenRemove(true); + handlePopoverClose(); + }; + + const handleCloseRemove = () => setOpenRemove(false); + + const submitRemove = () => { + return dispatch( + removeExerciseTeams(exerciseId, { + exercise_teams: [team.team_id], + }), + ).then(() => handleCloseRemove()); + }; + + const initialValues: TeamInputForm = { + ...team, + team_organization: organizationOption( + team.team_organization, + organizationsMap, + ), + team_tags: tagOptions(team.team_tags, tagsMap), + }; + return ( + <> + + + + + {t('Update')} + {exerciseId && !onRemoveTeam && ( + + {t('Remove from the exercise')} + + )} + {onRemoveTeam && ( + onRemoveTeam(team.team_id)}> + {t('Remove from the inject')} + + )} + {t('Delete')} + + + + + {t('Do you want to delete this team?')} + + + + + + + + + + + + + + {t('Do you want to remove the team from the simulation?')} + + + + + + + + + ); +}; + +export default TeamPopover; diff --git a/openex-front/src/components/PlayerField.js b/openex-front/src/components/PlayerField.js index 6b18e41393..36b37167a7 100644 --- a/openex-front/src/components/PlayerField.js +++ b/openex-front/src/components/PlayerField.js @@ -4,7 +4,7 @@ import { PersonOutlined } from '@mui/icons-material'; import { Box, Dialog, DialogTitle, DialogContent } from '@mui/material'; import withStyles from '@mui/styles/withStyles'; import { connect } from 'react-redux'; -import PlayerForm from '../admin/components/players/PlayerForm'; +import PlayerForm from '../admin/components/persons/players/PlayerForm'; import { addPlayer, fetchPlayers } from '../actions/User'; import Autocomplete from './Autocomplete'; import inject18n from './i18n'; diff --git a/openex-front/src/components/UserField.js b/openex-front/src/components/UserField.js index 7e5df8274e..2ca060b832 100644 --- a/openex-front/src/components/UserField.js +++ b/openex-front/src/components/UserField.js @@ -4,7 +4,7 @@ import { PersonOutlined } from '@mui/icons-material'; import { Box, Dialog, DialogTitle, DialogContent } from '@mui/material'; import withStyles from '@mui/styles/withStyles'; import { connect } from 'react-redux'; -import PlayerForm from '../admin/components/players/PlayerForm'; +import PlayerForm from '../admin/components/persons/players/PlayerForm'; import { fetchUsers, addUser } from '../actions/User'; import { fetchOrganizations } from '../actions/Organization'; import Autocomplete from './Autocomplete'; diff --git a/openex-front/src/constants/ComponentTypes.js b/openex-front/src/constants/ComponentTypes.js index e6dfd537c4..cf0a063fc5 100644 --- a/openex-front/src/constants/ComponentTypes.js +++ b/openex-front/src/constants/ComponentTypes.js @@ -98,7 +98,7 @@ export const APPBAR_TYPE_LEFTBAR = 'APPBAR_TYPE_LEFTBAR'; // region TOOLBAR export const TOOLBAR_TYPE_LOGIN = 'TOOLBAR_TYPE_LOGIN'; export const TOOLBAR_TYPE_EVENT = 'TOOLBAR_TYPE_EVENT'; -export const TOOLBAR_TYPE_AUDIENCE = 'TOOLBAR_TYPE_AUDIENCE'; +export const TOOLBAR_TYPE_TEAM = 'TOOLBAR_TYPE_TEAM'; // endregion // region PAPER diff --git a/openex-front/src/reducers/Referential.js b/openex-front/src/reducers/Referential.js index 38c66af216..018f790ffd 100644 --- a/openex-front/src/reducers/Referential.js +++ b/openex-front/src/reducers/Referential.js @@ -19,7 +19,7 @@ export const entitiesInitializer = Immutable({ challengesreaders: Immutable({}), dryruns: Immutable({}), dryinjects: Immutable({}), - audiences: Immutable({}), + teams: Immutable({}), injects: Immutable({}), inject_types: Immutable({}), inject_statuses: Immutable({}), diff --git a/openex-front/src/utils/Exercise.js b/openex-front/src/utils/Exercise.js index 2556c5b05f..bd5d1a0070 100644 --- a/openex-front/src/utils/Exercise.js +++ b/openex-front/src/utils/Exercise.js @@ -51,11 +51,11 @@ export const usePermissions = (exerciseId, fullExercise = null) => { || (exercise || fullExercise).exercise_status === 'CANCELED' || (exercise || fullExercise).exercise_planners?.includes(me.user_id); const canPlayBypassStatus = logged.admin - || (exercise || fullExercise).exercise_players?.includes(me.user_id); + || (exercise || fullExercise).exercise_users?.includes(me.user_id); const canPlay = logged.admin || (exercise || fullExercise).exercise_status === 'FINISHED' || (exercise || fullExercise).exercise_status === 'CANCELED' - || (exercise || fullExercise).exercise_players?.includes(me.user_id); + || (exercise || fullExercise).exercise_users?.includes(me.user_id); return { canRead, canWrite, diff --git a/openex-front/src/utils/Localization.js b/openex-front/src/utils/Localization.js index 7a06428e2d..5eb653ff30 100644 --- a/openex-front/src/utils/Localization.js +++ b/openex-front/src/utils/Localization.js @@ -25,7 +25,7 @@ const i18n = { Organizations: 'Organisations', Documents: 'Documents', Integrations: 'Intégrations', - Settings: 'Configuration', + Settings: 'Paramètres', Profile: 'Profil', Logout: 'Se déconnecter', Firstname: 'Prénom', @@ -54,7 +54,7 @@ const i18n = { 'Create a new media': 'Créer un nouveau média', 'Update the media': 'Modifier le média', 'Recent exercises': 'Exercices récents', - 'No audiences in this exercise.': 'Aucune audience dans cet exercice.', + 'No teams in this exercise.': 'Aucune team dans cet exercice.', 'No exercises in this platform.': 'Aucun exercice dans cette plateforme.', 'Injects distribution': 'Distribution des stimulis', 'Next injects to send': 'Prochains stimlis à envoyer', @@ -65,8 +65,8 @@ const i18n = { 'added an entry on': 'a ajouté une entrée le', 'This file type is not accepted here.': "Ce type de fichier n'est pas accepté ici", - 'Distribution of score by audience (in % of expectations)': - 'Distribution du score par audience (en % des attendus)', + 'Distribution of score by team (in % of expectations)': + 'Distribution du score par team (en % des attendus)', 'Select all': 'Sélectionner tout', 'Do you want to anonymize lessons learned questionnaire?': "Souhaitez-vous anonymiser le questionnaire de retour d'expérience ?", @@ -89,8 +89,8 @@ const i18n = { 'Inject details': 'Détails du stimuli', 'Export this list': 'Exporter cette liste', 'Create a new exercise': 'Créer un nouvel exercice', - 'Do you want to delete this audience?': - 'Souhaitez-vous supprimer cette audience ?', + 'Do you want to delete this team?': + 'Souhaitez-vous supprimer cette team ?', 'Do you want to delete this comcheck?': 'Souhaitez-vous supprimer cette vérification ?', 'Do you want to delete this player?': @@ -111,8 +111,8 @@ const i18n = { 'Do you want to delete this organization?': 'Souhaitez-vous supprimer cette organisation ?', 'Mail sent to': 'Mail envoyé à', - 'Audiences scores over time (in % of expectations)': - 'Score des audience dans le temps (en % des attendus)', + 'Teams scores over time (in % of expectations)': + 'Score des team dans le temps (en % des attendus)', 'Distribution of total score by inject type': 'Distribution du score total par type de stimuli', on: 'le', @@ -203,31 +203,41 @@ const i18n = { Seconds: 'Secondes', 'Try the inject': 'Tester le stimuli', File: 'Fichier', + Simulations: 'Simulations', + Simulation: 'Simulation', + Scenarios: 'Scenarios', + Detection: 'Détection', + Detections: 'Détections', + Assets: 'Actifs', + Persons: 'Personnes', + 'This field is mandatory': 'Ce champ est requis', + 'Remove from the exercise': 'Retirer de cet exercice', 'Select a file': 'Sélectionner un fichier', 'Do you want to delete this document?': 'Souhaitez-vous supprimer ce document ?', 'Update the document': 'Modifier le document', 'Inject data': 'Données du stimuli', - 'Add target audiences in this inject': - 'Ajouter des audiences cibles dans ce stimuli', - 'Targeted audiences': 'Audiences ciblées', + 'Add teams in this exercise': 'Ajouter une équipe dans cet exercice', + 'Add target teams in this inject': + 'Ajouter des teams cibles dans ce stimuli', + 'Targeted teams': 'Teams ciblées', 'Test the inject': 'Tester le stimuli', - 'All audiences': 'Toutes les audiences', - 'Add target audiences': 'Ajouter des audiences cibles', + 'All teams': 'Toutes les teams', + 'Add target teams': 'Ajouter des teams cibles', 'Trigger after': 'Se déclenche après', 'Create a new inject': 'Créer un nouveau stimuli', - 'Remove from the audience': "Retirer de l'audience", - 'Update the audience': "Modifier l'audience", + 'Remove from the team': "Retirer de l'team", + 'Update the team': "Modifier l'team", 'Update the exercise': "Modifier l'exercice", 'Update the inject': 'Modifier le stimuli', - 'Do you want to remove the player from the audience?': - 'Souhaitez-vous retirer le joueur de cette audience ?', - 'Add players in this audience': 'Ajouter des joueurs dans cette audience', - 'Create a new audience': 'Créer une nouvelle audience', + 'Do you want to remove the player from the team?': + 'Souhaitez-vous retirer le joueur de cette team ?', + 'Add players in this team': 'Ajouter des joueurs dans cette team', + 'Create a new team': 'Créer une nouvelle team', 'Remove from the inject': 'Retirer du stimuli', 'Remove from the element': "Retirer de l'élément", - 'Do you want to remove the audience from the inject?': - "Souhaitez-vous retirer l'audience du stimuli ?", + 'Do you want to remove the team from the inject?': + "Souhaitez-vous retirer l'team du stimuli ?", 'Do you want to delete this exercise?': 'Souhaitez-vous supprimer cet exercice ?', 'Number of injects': 'Nombre de stimulis', @@ -251,7 +261,7 @@ const i18n = { 'Ce groupe aura la permission de planificateur sur les nouveaux exercices.', 'Processed injects': 'Stimulis traités', 'Pending injects': 'Stimulis en attente', - 'No audience': 'Aucune audience', + 'No team': 'Aucune team', 'No processed injects in this exercise.': 'Aucun stimuli traité dans cet exercice.', 'No pending injects in this exercise.': @@ -272,12 +282,12 @@ const i18n = { Attachments: 'Pièces jointes', Attachment: 'Pièce jointe', Content: 'Contenu', - Audiences: 'Audiences', + Teams: 'Teams', 'Send email': 'Envoyer le mail', 'Add documents in this media pressure': 'Ajouter des documents à cette pression médiatique', - 'Expect audiences to read the article(s)': - 'Les audiences doivent lire le(s) article(s)', + 'Expect teams to read the article(s)': + 'Les teams doivent lire le(s) article(s)', 'Add media pressure': 'Ajouter de la pression médiatique', 'Remove from the medias pressure': 'Supprimer de la pression médiatique', 'Raw request data': 'Données brutes de la requête', @@ -285,7 +295,7 @@ const i18n = { 'Form request data': 'Données de formulaire de la requête', Key: 'Clé', Headers: 'En-têtes', - audience: 'Audience', + team: 'Team', attachment: 'Document', 'Executed in': 'Exécuté en', 'Use basic authentication': 'Utiliser une authentification basique', @@ -323,10 +333,10 @@ const i18n = { 'Souhaitez-vous désactiver ce stimuli ?', 'Do you want to enable this inject?': 'Souhaitez-vous activer ce stimuli ?', - 'Do you want to disable this audience?': - 'Souhaitez-vous désactiver cette audience ?', - 'Do you want to enable this audience?': - 'Souhaitez-vous activer cette audience ?', + 'Do you want to disable this team?': + 'Souhaitez-vous désactiver cette team ?', + 'Do you want to enable this team?': + 'Souhaitez-vous activer cette team ?', Trigger: 'Déclencheur', 'Manage content': 'Gérer le contenu', Controls: 'Contrôles', @@ -495,8 +505,8 @@ const i18n = { 'Questionnaire mode': 'Mode du questionnaire', 'Anonymize answers': 'Anonymiser les réponses', 'No explanation': 'Aucune explication', - 'Add target audiences in this lessons learned category': - "Ajouter des audiences cibles à cette catégorie de retour d'expérience", + 'Add target teams in this lessons learned category': + "Ajouter des teams cibles à cette catégorie de retour d'expérience", 'Sending the questionnaire will emit an email to each player with a unique link to access and fill it.': 'Envoyer le questionnaire va émettre un email à chaque joueur avec un lien unique pour y accéder et le remplir.', 'Do you want to empty lessons learned categories and questions?': @@ -542,19 +552,19 @@ const i18n = { 'No category': 'Aucune catégorie', 'No challenge in this exercise yet.': 'Encore aucun challenge dans cet exercice.', - 'Distribution of expectations by audience': - 'Distribution des attendus par audience', + 'Distribution of expectations by team': + 'Distribution des attendus par team', 'Distribution of expectations by inject type': 'Distribution des attendus par type de stimuli', 'Distribution of expected total score by inject type': 'Distribution du score total attendu par type de stimuli', '-': 'Aucun', - 'Each audience should upload a document': - 'Chaque audience doit uploader un document', - 'Each audience should submit a text response': - 'Chaque audience doit soumettre une réponse texte', + 'Each team should upload a document': + 'Chaque team doit uploader un document', + 'Each team should submit a text response': + 'Chaque team doit soumettre une réponse texte', // -- Expectation start -- - 'This expectation is handled automatically by the platform and triggered when audience reads articles': 'Cet attendu est géré automatiquement par la plateforme et déclenché lorsque une audience lit les articles', + 'This expectation is handled automatically by the platform and triggered when team reads articles': 'Cet attendu est géré automatiquement par la plateforme et déclenché lorsque une team lit les articles', 'Add expectations': 'Ajouter des attendus', 'Add expectation in this inject': 'Ajouter des attendus dans ce stimuli', 'Update the expectation': 'Modifier l\'attendu', @@ -566,14 +576,14 @@ const i18n = { MANUAL: 'Manuel', ARTICLE: 'Attendu lié à aux articles', // -- Expectation end -- - 'Distribution of expected total score by audience': - 'Distribution du score total attendu par audience', + 'Distribution of expected total score by team': + 'Distribution du score total attendu par team', 'Exercise definition and scenario': "Définition de l'exercice et scénario", 'Exercise results': "Résultats de l'exercice", 'Exercise data': "Données de l'exercice", - 'Distribution of total score by audience': - 'Distribution du score total par audience', + 'Distribution of total score by team': + 'Distribution du score total par team', 'Distribution of total score by organization': 'Distribution du score total par organisation', 'Total expected score': 'Score total attendu', @@ -582,10 +592,10 @@ const i18n = { 'Distribution of total score by inject': 'Distribution du score total par stimuli', 'Distribution of injects by type': 'Distribution des stimulis par type', - 'Distribution of injects by audience': - 'Distribution des stimulis par audience', - 'Distribution of mails by audience': - 'Distribution des mails par audience', + 'Distribution of injects by team': + 'Distribution des stimulis par team', + 'Distribution of mails by team': + 'Distribution des mails par team', 'Distribution of mails by player': 'Distribution des mails par joueur', 'Distribution of mails by inject': 'Distribution des mails par stimuli', 'Sent injects over time': 'Stimulis envoyés dans le temps', @@ -593,11 +603,11 @@ const i18n = { 'No data to display or the exercise has not started yet': "Aucune donnée disponible ou l'exercice n'a pas encore commencé", 'Back to administration': "Retour à l'administration", - 'Audiences scores over time': 'Score des audiences dans le temps', + 'Teams scores over time': 'Score des teams dans le temps', 'Inject types scores over time': 'Score des types de stimuli dans le temps', - 'The animation team can validate the audience reaction': - "L'équipe d'animation peut valider la réaction de l'audience", + 'The animation team can validate the team reaction': + "L'équipe d'animation peut valider la réaction de l'team", Description: 'Description', 'No description': 'Aucune description', 'Only injects with manual validation': @@ -654,7 +664,7 @@ const i18n = { 'Quick inject definition': 'Définition rapide de l’injection', Order: 'Ordre', Details: 'Détails', - Audience: 'Public', + Team: 'Public', Template: 'Modèle', Questionnaire: 'Questionnaire', User: 'Utilisateur', @@ -692,8 +702,8 @@ const i18n = { Body: 'Email body', Encrypted: 'Encrypt this email', Attachments: 'Attachments', - Audiences: 'Audiences', - audience: 'Audience', + Teams: 'Teams', + team: 'Team', text: 'Text field', textarea: 'Text area', tuple: 'Key value pair', @@ -714,7 +724,7 @@ const i18n = { REGEXP: 'Regular expression', '-': 'None', MANUAL: 'Manual', - ARTICLE: 'Automatic - Triggered when audience reads articles', + ARTICLE: 'Automatic - Triggered when team reads articles', }, }, }; diff --git a/openex-front/src/utils/api-types.d.ts b/openex-front/src/utils/api-types.d.ts index 46bf5b88a2..435c726870 100644 --- a/openex-front/src/utils/api-types.d.ts +++ b/openex-front/src/utils/api-types.d.ts @@ -63,50 +63,6 @@ export interface ArticleUpdateInput { article_shares?: number; } -export interface Audience { - audience_communications?: Communication[]; - /** @format date-time */ - audience_created_at?: string; - audience_description?: string; - audience_enabled?: boolean; - audience_exercise?: Exercise; - audience_id?: string; - audience_inject_expectations?: InjectExpectation[]; - audience_injects?: Inject[]; - /** @format int64 */ - audience_injects_expectations_number?: number; - /** @format int64 */ - audience_injects_expectations_total_expected_score?: number; - /** @format int64 */ - audience_injects_expectations_total_score?: number; - /** @format int64 */ - audience_injects_number?: number; - audience_name?: string; - audience_tags?: Tag[]; - /** @format date-time */ - audience_updated_at?: string; - audience_users?: User[]; - /** @format int64 */ - audience_users_number?: number; - updateAttributes?: object; -} - -export interface AudienceCreateInput { - audience_description?: string; - audience_name: string; - audience_tags?: string[]; -} - -export interface AudienceUpdateActivationInput { - audience_enabled?: boolean; -} - -export interface AudienceUpdateInput { - audience_description?: string; - audience_name: string; - audience_tags?: string[]; -} - export interface Challenge { challenge_category?: string; challenge_content?: string; @@ -209,12 +165,12 @@ export interface Comcheck { } export interface ComcheckInput { - comcheck_audiences?: string[]; /** @format date-time */ comcheck_end_date?: string; comcheck_message?: string; comcheck_name: string; comcheck_subject?: string; + comcheck_teams?: string[]; } export interface ComcheckStatus { @@ -287,7 +243,7 @@ export interface ContractElement { | "challenge" | "dependency-select" | "attachment" - | "audience" + | "team" | "expectation"; } @@ -441,6 +397,8 @@ export interface ExecutionTrace { } export interface Exercise { + /** @format int64 */ + exercise_all_users_number?: number; exercise_articles?: Article[]; /** @format int64 */ exercise_communications_number?: number; @@ -471,7 +429,6 @@ export interface Exercise { exercise_observers?: User[]; exercise_pauses?: Pause[]; exercise_planners?: User[]; - exercise_players?: User[]; /** @format double */ exercise_score?: number; /** @format date-time */ @@ -479,8 +436,11 @@ export interface Exercise { exercise_status?: "SCHEDULED" | "CANCELED" | "RUNNING" | "PAUSED" | "FINISHED"; exercise_subtitle?: string; exercise_tags?: Tag[]; + exercise_teams?: Team[]; + exercise_teams_users?: ExerciseTeamUser[]; /** @format date-time */ exercise_updated_at?: string; + exercise_users?: User[]; /** @format int64 */ exercise_users_number?: number; updateAttributes?: object; @@ -509,6 +469,16 @@ export interface ExerciseSimple { exercise_tags?: Tag[]; } +export interface ExerciseTeamPlayersEnableInput { + exercise_team_players?: string[]; +} + +export interface ExerciseTeamUser { + exercise_id?: Exercise; + team_id?: Team; + user_id?: User; +} + export interface ExerciseUpdateInput { exercise_description?: string; exercise_mail_from?: string; @@ -536,8 +506,11 @@ export interface ExerciseUpdateTagsInput { exercise_tags?: string[]; } +export interface ExerciseUpdateTeamsInput { + exercise_teams?: string[]; +} + export interface ExpectationUpdateInput { - expectation_id?: string; /** @format int32 */ expectation_score: number; } @@ -589,8 +562,7 @@ export interface GroupUpdateUsersInput { export interface Inject { footer?: string; header?: string; - inject_all_audiences?: boolean; - inject_audiences?: Audience[]; + inject_all_teams?: boolean; inject_city?: string; inject_communications?: Communication[]; /** @format int64 */ @@ -620,6 +592,7 @@ export interface Inject { inject_sent_at?: string; inject_status?: InjectStatus; inject_tags?: Tag[]; + inject_teams?: Team[]; inject_title?: string; inject_type?: string; /** @format date-time */ @@ -630,10 +603,6 @@ export interface Inject { updateAttributes?: object; } -export interface InjectAudiencesInput { - inject_audiences?: string[]; -} - export interface InjectDocument { document_attached?: boolean; document_id?: Document; @@ -647,7 +616,6 @@ export interface InjectDocumentInput { export interface InjectExpectation { inject_expectation_article?: Article; - inject_expectation_audience?: Audience; inject_expectation_challenge?: Challenge; /** @format date-time */ inject_expectation_created_at?: string; @@ -660,6 +628,7 @@ export interface InjectExpectation { inject_expectation_result?: string; /** @format int32 */ inject_expectation_score?: number; + inject_expectation_team?: Team; inject_expectation_type?: "TEXT" | "DOCUMENT" | "ARTICLE" | "CHALLENGE" | "MANUAL"; /** @format date-time */ inject_expectation_updated_at?: string; @@ -669,8 +638,7 @@ export interface InjectExpectation { } export interface InjectInput { - inject_all_audiences?: boolean; - inject_audiences?: string[]; + inject_all_teams?: boolean; inject_city?: string; inject_content?: object; inject_contract?: string; @@ -681,6 +649,7 @@ export interface InjectInput { inject_description?: string; inject_documents?: InjectDocumentInput[]; inject_tags?: string[]; + inject_teams?: string[]; inject_title?: string; } @@ -696,6 +665,10 @@ export interface InjectStatus { updateAttributes?: object; } +export interface InjectTeamsInput { + inject_teams?: string[]; +} + export interface InjectUpdateActivationInput { inject_enabled?: boolean; } @@ -734,7 +707,6 @@ export interface LessonsAnswerCreateInput { } export interface LessonsCategory { - lessons_category_audiences?: Audience[]; /** @format date-time */ lessons_category_created_at?: string; lessons_category_description?: string; @@ -743,6 +715,7 @@ export interface LessonsCategory { /** @format int32 */ lessons_category_order?: number; lessons_category_questions?: LessonsQuestion[]; + lessons_category_teams?: Team[]; /** @format date-time */ lessons_category_updated_at?: string; lessons_category_users?: string[]; @@ -750,10 +723,6 @@ export interface LessonsCategory { updateAttributes?: object; } -export interface LessonsCategoryAudiencesInput { - lessons_category_audiences?: string[]; -} - export interface LessonsCategoryCreateInput { lessons_category_description?: string; lessons_category_name: string; @@ -761,6 +730,10 @@ export interface LessonsCategoryCreateInput { lessons_category_order?: number; } +export interface LessonsCategoryTeamsInput { + lessons_category_teams?: string[]; +} + export interface LessonsCategoryUpdateInput { lessons_category_description?: string; lessons_category_name: string; @@ -894,7 +867,7 @@ export interface LinkedFieldModel { | "challenge" | "dependency-select" | "attachment" - | "audience" + | "team" | "expectation"; } @@ -1163,6 +1136,49 @@ export interface TagUpdateInput { tag_name: string; } +export interface Team { + team_communications?: Communication[]; + /** @format date-time */ + team_created_at?: string; + team_description?: string; + team_exercises?: Exercise[]; + team_exercises_users?: ExerciseTeamUser[]; + team_id?: string; + team_inject_expectations?: InjectExpectation[]; + team_injects?: Inject[]; + /** @format int64 */ + team_injects_expectations_number?: number; + /** @format int64 */ + team_injects_expectations_total_expected_score?: number; + /** @format int64 */ + team_injects_expectations_total_score?: number; + /** @format int64 */ + team_injects_number?: number; + team_name: string; + team_organization?: Organization; + team_tags?: Tag[]; + /** @format date-time */ + team_updated_at?: string; + team_users?: User[]; + /** @format int64 */ + team_users_number?: number; + updateAttributes?: object; +} + +export interface TeamCreateInput { + team_description?: string; + team_name: string; + team_organization?: string; + team_tags?: string[]; +} + +export interface TeamUpdateInput { + team_description?: string; + team_name: string; + team_organization?: string; + team_tags?: string[]; +} + export interface Token { /** @format date-time */ token_created_at?: string; @@ -1218,8 +1234,8 @@ export interface UpdateUserInput { user_tags?: string[]; } -export interface UpdateUsersAudienceInput { - audience_users?: string[]; +export interface UpdateUsersTeamInput { + team_users?: string[]; } export interface User { @@ -1227,7 +1243,6 @@ export interface User { injects?: Inject[]; updateAttributes?: object; user_admin?: boolean; - user_audiences?: Audience[]; user_city?: string; user_communications?: Communication[]; user_country?: string; @@ -1257,6 +1272,7 @@ export interface User { /** @format int32 */ user_status: number; user_tags?: Tag[]; + user_teams?: Team[]; user_theme?: string; /** @format date-time */ user_updated_at?: string; diff --git a/openex-injectors b/openex-injectors index 7912284738..b68acb867d 160000 --- a/openex-injectors +++ b/openex-injectors @@ -1 +1 @@ -Subproject commit 791228473876f3e31c2559da1cf12fca36629239 +Subproject commit b68acb867df23c61101d79570d47c8a8e6ba7720 diff --git a/openex-model/src/main/java/io/openex/database/model/Exercise.java b/openex-model/src/main/java/io/openex/database/model/Exercise.java index 5ffeccb2cd..e6fc2b3276 100644 --- a/openex-model/src/main/java/io/openex/database/model/Exercise.java +++ b/openex-model/src/main/java/io/openex/database/model/Exercise.java @@ -6,6 +6,9 @@ import io.openex.database.audit.ModelBaseListener; import io.openex.helper.MonoIdDeserializer; import io.openex.helper.MultiIdDeserializer; +import io.openex.helper.MultiModelDeserializer; +import lombok.Getter; +import lombok.Setter; import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; @@ -105,10 +108,6 @@ public enum STATUS { @JsonProperty("exercise_updated_at") private Instant updatedAt = now(); - @OneToMany(mappedBy = "exercise", fetch = FetchType.LAZY) - @JsonIgnore - private List audiences = new ArrayList<>(); - @OneToMany(mappedBy = "exercise", fetch = FetchType.EAGER) @JsonIgnore private List grants = new ArrayList<>(); @@ -118,6 +117,21 @@ public enum STATUS { @JsonSerialize(using = MultiIdDeserializer.class) private List injects = new ArrayList<>(); + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "exercises_teams", + joinColumns = @JoinColumn(name = "exercise_id"), + inverseJoinColumns = @JoinColumn(name = "team_id")) + @JsonSerialize(using = MultiIdDeserializer.class) + @JsonProperty("exercise_teams") + private List teams = new ArrayList<>(); + + @Getter + @Setter + @OneToMany(mappedBy = "exercise", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("exercise_teams_users") + @JsonSerialize(using = MultiModelDeserializer.class) + private List teamUsers = new ArrayList<>(); + @OneToMany(mappedBy = "exercise", fetch = FetchType.LAZY) @JsonIgnore private List objectives = new ArrayList<>(); @@ -215,17 +229,20 @@ public boolean isUserHasAccess(User user) { return user.isAdmin() || getObservers().contains(user); } + @JsonProperty("exercise_all_users_number") + public long usersAllNumber() { + return getTeams().stream().mapToLong(Team::getUsersNumber).sum(); + } + @JsonProperty("exercise_users_number") public long usersNumber() { - return getAudiences().stream().flatMap(audience -> audience.getUsers().stream()).distinct().count(); + return getTeamUsers().stream().map(ExerciseTeamUser::getUser).distinct().count(); } - @JsonProperty("exercise_players") + @JsonProperty("exercise_users") @JsonSerialize(using = MultiIdDeserializer.class) - public List getPlayers() { - return getAudiences().stream().flatMap(audience -> audience.getUsers().stream()) - .distinct() - .toList(); + public List getUsers() { + return getTeamUsers().stream().map(ExerciseTeamUser::getUser).distinct().toList(); } @JsonProperty("exercise_score") @@ -401,20 +418,28 @@ public void setInjects(List injects) { this.injects = injects; } - public List getPauses() { - return pauses; + public List getTeams() { + return teams; } - public void setPauses(List pauses) { - this.pauses = pauses; + public void setTeams(List teams) { + this.teams = teams; + } + + public List getTeamUsers() { + return teamUsers; } - public List getAudiences() { - return audiences; + public void setTeamUsers(List teamUsers) { + this.teamUsers = teamUsers; } - public void setAudiences(List audiences) { - this.audiences = audiences; + public List getPauses() { + return pauses; + } + + public void setPauses(List pauses) { + this.pauses = pauses; } public List getGrants() { diff --git a/openex-model/src/main/java/io/openex/database/model/ExerciseTeamUser.java b/openex-model/src/main/java/io/openex/database/model/ExerciseTeamUser.java new file mode 100644 index 0000000000..54ea002e62 --- /dev/null +++ b/openex-model/src/main/java/io/openex/database/model/ExerciseTeamUser.java @@ -0,0 +1,83 @@ +package io.openex.database.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.openex.helper.MonoIdDeserializer; + +import javax.persistence.*; +import java.util.Objects; + +@Entity +@Table(name = "exercises_teams_users") +public class ExerciseTeamUser { + @EmbeddedId + @JsonIgnore + private ExerciseTeamUserId compositeId = new ExerciseTeamUserId(); + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("exerciseId") + @JoinColumn(name = "exercise_id") + @JsonProperty("exercise_id") + @JsonSerialize(using = MonoIdDeserializer.class) + private Exercise exercise; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("teamId") + @JoinColumn(name = "team_id") + @JsonProperty("team_id") + @JsonSerialize(using = MonoIdDeserializer.class) + private Team team; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("userId") + @JoinColumn(name = "user_id") + @JsonProperty("user_id") + @JsonSerialize(using = MonoIdDeserializer.class) + private User user; + + public ExerciseTeamUserId getCompositeId() { + return compositeId; + } + + public void setCompositeId(ExerciseTeamUserId compositeId) { + this.compositeId = compositeId; + } + + public Exercise getExercise() { + return exercise; + } + + public void setExercise(Exercise exercise) { + this.exercise = exercise; + } + + public Team getTeam() { + return team; + } + + public void setTeam(Team team) { + this.team = team; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExerciseTeamUser that = (ExerciseTeamUser) o; + return compositeId.equals(that.compositeId); + } + + @Override + public int hashCode() { + return Objects.hash(compositeId); + } +} diff --git a/openex-model/src/main/java/io/openex/database/model/ExerciseTeamUserId.java b/openex-model/src/main/java/io/openex/database/model/ExerciseTeamUserId.java new file mode 100644 index 0000000000..251f51cdf9 --- /dev/null +++ b/openex-model/src/main/java/io/openex/database/model/ExerciseTeamUserId.java @@ -0,0 +1,58 @@ +package io.openex.database.model; + +import javax.persistence.Embeddable; +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class ExerciseTeamUserId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String exerciseId; + private String teamId; + private String userId; + + public ExerciseTeamUserId() { + // Default constructor + } + + public String getExerciseId() { + return exerciseId; + } + + public void setExerciseId(String exerciseId) { + this.exerciseId = exerciseId; + } + + public String getTeamId() { + return teamId; + } + + public void setTeamId(String teamId) { + this.teamId = teamId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExerciseTeamUserId that = (ExerciseTeamUserId) o; + return exerciseId.equals(that.exerciseId) && teamId.equals(that.teamId) && userId.equals(that.userId); + } + + @Override + public int hashCode() { + return Objects.hash(exerciseId, teamId, userId); + } +} diff --git a/openex-model/src/main/java/io/openex/database/model/Inject.java b/openex-model/src/main/java/io/openex/database/model/Inject.java index 34dcb28c43..f9561ccc71 100644 --- a/openex-model/src/main/java/io/openex/database/model/Inject.java +++ b/openex-model/src/main/java/io/openex/database/model/Inject.java @@ -28,309 +28,309 @@ @EntityListeners(ModelBaseListener.class) public class Inject implements Base, Injection { - public static final int SPEED_STANDARD = 1; // Standard speed define by the user. - - public static Comparator executionComparator = (o1, o2) -> { - if (o1.getDate().isPresent() && o2.getDate().isPresent()) { - return o1.getDate().get().compareTo(o2.getDate().get()); - } - return o1.getId().compareTo(o2.getId()); - }; - - @Getter - @Setter - @Id - @Column(name = "inject_id") - @GeneratedValue(generator = "UUID") - @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") - @JsonProperty("inject_id") - private String id; - - @Getter - @Setter - @Column(name = "inject_title") - @JsonProperty("inject_title") - private String title; - - @Getter - @Setter - @Column(name = "inject_description") - @JsonProperty("inject_description") - private String description; - - @Getter - @Setter - @Column(name = "inject_contract") - @JsonProperty("inject_contract") - private String contract; - - @Getter - @Setter - @Column(name = "inject_country") - @JsonProperty("inject_country") - private String country; - - @Getter - @Setter - @Column(name = "inject_city") - @JsonProperty("inject_city") - private String city; - - @Getter - @Setter - @Column(name = "inject_enabled") - @JsonProperty("inject_enabled") - private boolean enabled = true; - - @Getter - @Setter - @Column(name = "inject_type", updatable = false) - @JsonProperty("inject_type") - private String type; - - @Getter - @Setter - @Column(name = "inject_content") - @Convert(converter = ContentConverter.class) - @JsonProperty("inject_content") - private ObjectNode content; - - @Getter - @Setter - @Column(name = "inject_created_at") - @JsonProperty("inject_created_at") - private Instant createdAt = now(); - - @Getter - @Setter - @Column(name = "inject_updated_at") - @JsonProperty("inject_updated_at") - private Instant updatedAt = now(); - - @Getter - @Setter - @Column(name = "inject_all_audiences") - @JsonProperty("inject_all_audiences") - private boolean allAudiences; - - @Getter - @Setter - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "inject_exercise") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_exercise") - private Exercise exercise; - - @Getter - @Setter - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "inject_depends_from_another") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_depends_on") - private Inject dependsOn; - - @Getter - @Setter - @Column(name = "inject_depends_duration") - @JsonProperty("inject_depends_duration") + public static final int SPEED_STANDARD = 1; // Standard speed define by the user. + + public static Comparator executionComparator = (o1, o2) -> { + if (o1.getDate().isPresent() && o2.getDate().isPresent()) { + return o1.getDate().get().compareTo(o2.getDate().get()); + } + return o1.getId().compareTo(o2.getId()); + }; + + @Getter + @Setter + @Id + @Column(name = "inject_id") + @GeneratedValue(generator = "UUID") + @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") + @JsonProperty("inject_id") + private String id; + + @Getter + @Setter + @Column(name = "inject_title") + @JsonProperty("inject_title") + private String title; + + @Getter + @Setter + @Column(name = "inject_description") + @JsonProperty("inject_description") + private String description; + + @Getter + @Setter + @Column(name = "inject_contract") + @JsonProperty("inject_contract") + private String contract; + + @Getter + @Setter + @Column(name = "inject_country") + @JsonProperty("inject_country") + private String country; + + @Getter + @Setter + @Column(name = "inject_city") + @JsonProperty("inject_city") + private String city; + + @Getter + @Setter + @Column(name = "inject_enabled") + @JsonProperty("inject_enabled") + private boolean enabled = true; + + @Getter + @Setter + @Column(name = "inject_type", updatable = false) + @JsonProperty("inject_type") + private String type; + + @Getter + @Setter + @Column(name = "inject_content") + @Convert(converter = ContentConverter.class) + @JsonProperty("inject_content") + private ObjectNode content; + + @Getter + @Setter + @Column(name = "inject_created_at") + @JsonProperty("inject_created_at") + private Instant createdAt = now(); + + @Getter + @Setter + @Column(name = "inject_updated_at") + @JsonProperty("inject_updated_at") + private Instant updatedAt = now(); + + @Getter + @Setter + @Column(name = "inject_all_teams") + @JsonProperty("inject_all_teams") + private boolean allTeams; + + @Getter + @Setter + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "inject_exercise") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("inject_exercise") + private Exercise exercise; + + @Getter + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inject_depends_from_another") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("inject_depends_on") + private Inject dependsOn; + + @Getter + @Setter + @Column(name = "inject_depends_duration") + @JsonProperty("inject_depends_duration") @NotNull @Min(value = 0L, message = "The value must be positive") - private Long dependsDuration; - - @Getter - @Setter - @ManyToOne(fetch = FetchType.LAZY) - @JsonSerialize(using = MonoIdDeserializer.class) - @JoinColumn(name = "inject_user") - @JsonProperty("inject_user") - private User user; - - // CascadeType.ALL is required here because inject status are embedded - @Setter - @OneToOne(mappedBy = "inject", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_status") - private InjectStatus status; - - @Getter - @Setter - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "injects_tags", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "tag_id")) - @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("inject_tags") - private List tags = new ArrayList<>(); - - @Getter - @Setter - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "injects_audiences", - joinColumns = @JoinColumn(name = "inject_id"), - inverseJoinColumns = @JoinColumn(name = "audience_id")) - @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("inject_audiences") - private List audiences = new ArrayList<>(); - - // CascadeType.ALL is required here because of complex relationships - @Getter - @Setter - @OneToMany(mappedBy = "inject", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_documents") - @JsonSerialize(using = MultiModelDeserializer.class) - private List documents = new ArrayList<>(); - - // CascadeType.ALL is required here because communications are embedded - @Getter - @Setter - @OneToMany(mappedBy = "inject", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_communications") - @JsonSerialize(using = MultiModelDeserializer.class) - private List communications = new ArrayList<>(); - - // CascadeType.ALL is required here because expectations are embedded - @Getter - @Setter - @OneToMany(mappedBy = "inject", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JsonProperty("inject_expectations") - @JsonSerialize(using = MultiModelDeserializer.class) - private List expectations = new ArrayList<>(); - - // region transient - @Transient - public String getHeader() { - return ofNullable(this.getExercise()).map(Exercise::getHeader).orElse(""); - } - - @Transient - public String getFooter() { - return ofNullable(this.getExercise()).map(Exercise::getFooter).orElse(""); - } - - @JsonIgnore - @Override - public boolean isUserHasAccess(User user) { - return this.getExercise().isUserHasAccess(user); - } - - @JsonIgnore - public void clean() { - this.status = null; - this.communications.clear(); - this.expectations.clear(); - } - - @JsonProperty("inject_users_number") - public long getNumberOfTargetUsers() { - if (this.allAudiences) { - return getExercise().usersNumber(); + private Long dependsDuration; + + @Getter + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JsonSerialize(using = MonoIdDeserializer.class) + @JoinColumn(name = "inject_user") + @JsonProperty("inject_user") + private User user; + + // CascadeType.ALL is required here because inject status are embedded + @Setter + @OneToOne(mappedBy = "inject", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_status") + private InjectStatus status; + + @Getter + @Setter + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "injects_tags", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id")) + @JsonSerialize(using = MultiIdDeserializer.class) + @JsonProperty("inject_tags") + private List tags = new ArrayList<>(); + + @Getter + @Setter + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "injects_teams", + joinColumns = @JoinColumn(name = "inject_id"), + inverseJoinColumns = @JoinColumn(name = "team_id")) + @JsonSerialize(using = MultiIdDeserializer.class) + @JsonProperty("inject_teams") + private List teams = new ArrayList<>(); + + // CascadeType.ALL is required here because of complex relationships + @Getter + @Setter + @OneToMany(mappedBy = "inject", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_documents") + @JsonSerialize(using = MultiModelDeserializer.class) + private List documents = new ArrayList<>(); + + // CascadeType.ALL is required here because communications are embedded + @Getter + @Setter + @OneToMany(mappedBy = "inject", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_communications") + @JsonSerialize(using = MultiModelDeserializer.class) + private List communications = new ArrayList<>(); + + // CascadeType.ALL is required here because expectations are embedded + @Getter + @Setter + @OneToMany(mappedBy = "inject", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("inject_expectations") + @JsonSerialize(using = MultiModelDeserializer.class) + private List expectations = new ArrayList<>(); + + // region transient + @Transient + public String getHeader() { + return ofNullable(this.getExercise()).map(Exercise::getHeader).orElse(""); + } + + @Transient + public String getFooter() { + return ofNullable(this.getExercise()).map(Exercise::getFooter).orElse(""); + } + + @JsonIgnore + @Override + public boolean isUserHasAccess(User user) { + return this.getExercise().isUserHasAccess(user); + } + + @JsonIgnore + public void clean() { + this.status = null; + this.communications.clear(); + this.expectations.clear(); + } + + @JsonProperty("inject_users_number") + public long getNumberOfTargetUsers() { + if (this.allTeams) { + return getExercise().usersNumber(); + } + return getTeams().stream() + .map(team -> team.getUsersNumberInExercise(getExercise())) + .reduce(Long::sum).orElse(0L); + } + + @JsonIgnore + public Instant computeInjectDate(Instant source, int speed) { + // Compute origin execution date + Optional dependsOnInject = ofNullable(getDependsOn()); + long duration = ofNullable(getDependsDuration()).orElse(0L) / speed; + Instant dependingStart = dependsOnInject + .map(inject -> inject.computeInjectDate(source, speed)) + .orElse(source); + Instant standardExecutionDate = dependingStart.plusSeconds(duration); + // Compute execution dates with previous terminated pauses + long previousPauseDelay = exercise.getPauses().stream() + .filter(pause -> pause.getDate().isBefore(standardExecutionDate)) + .mapToLong(pause -> pause.getDuration().orElse(0L)).sum(); + Instant afterPausesExecutionDate = standardExecutionDate.plusSeconds(previousPauseDelay); + // Add current pause duration in date computation if needed + long currentPauseDelay = exercise.getCurrentPause() + .map(last -> last.isBefore(afterPausesExecutionDate) ? between(last, now()).getSeconds() : 0L) + .orElse(0L); + long globalPauseDelay = previousPauseDelay + currentPauseDelay; + long minuteAlignModulo = globalPauseDelay % 60; + long alignedPauseDelay = minuteAlignModulo > 0 ? globalPauseDelay + (60 - minuteAlignModulo) : globalPauseDelay; + return standardExecutionDate.plusSeconds(alignedPauseDelay); + } + + @JsonProperty("inject_date") + public Optional getDate() { + if (this.getExercise().getStatus().equals(Exercise.STATUS.CANCELED)) { + return Optional.empty(); + } + return this.getExercise().getStart() + .map(source -> computeInjectDate(source, SPEED_STANDARD)); + } + + @JsonIgnore + public boolean isNotExecuted() { + return this.getStatus().isEmpty(); + } + + @JsonIgnore + public boolean isPastInject() { + return this.getDate().map(date -> date.isBefore(now())).orElse(false); } - return getAudiences().stream() - .map(Audience::getUsersNumber) - .reduce(Long::sum).orElse(0L); - } - - @JsonIgnore - public Instant computeInjectDate(Instant source, int speed) { - // Compute origin execution date - Optional dependsOnInject = ofNullable(getDependsOn()); - long duration = ofNullable(getDependsDuration()).orElse(0L) / speed; - Instant dependingStart = dependsOnInject - .map(inject -> inject.computeInjectDate(source, speed)) - .orElse(source); - Instant standardExecutionDate = dependingStart.plusSeconds(duration); - // Compute execution dates with previous terminated pauses - long previousPauseDelay = exercise.getPauses().stream() - .filter(pause -> pause.getDate().isBefore(standardExecutionDate)) - .mapToLong(pause -> pause.getDuration().orElse(0L)).sum(); - Instant afterPausesExecutionDate = standardExecutionDate.plusSeconds(previousPauseDelay); - // Add current pause duration in date computation if needed - long currentPauseDelay = exercise.getCurrentPause() - .map(last -> last.isBefore(afterPausesExecutionDate) ? between(last, now()).getSeconds() : 0L) - .orElse(0L); - long globalPauseDelay = previousPauseDelay + currentPauseDelay; - long minuteAlignModulo = globalPauseDelay % 60; - long alignedPauseDelay = minuteAlignModulo > 0 ? globalPauseDelay + (60 - minuteAlignModulo) : globalPauseDelay; - return standardExecutionDate.plusSeconds(alignedPauseDelay); - } - - @JsonProperty("inject_date") - public Optional getDate() { - if (this.getExercise().getStatus().equals(Exercise.STATUS.CANCELED)) { - return Optional.empty(); + + @JsonIgnore + public boolean isFutureInject() { + return this.getDate().map(date -> date.isAfter(now())).orElse(false); } - return this.getExercise().getStart() - .map(source -> computeInjectDate(source, SPEED_STANDARD)); - } - - @JsonIgnore - public boolean isNotExecuted() { - return this.getStatus().isEmpty(); - } - - @JsonIgnore - public boolean isPastInject() { - return this.getDate().map(date -> date.isBefore(now())).orElse(false); - } - - @JsonIgnore - public boolean isFutureInject() { - return this.getDate().map(date -> date.isAfter(now())).orElse(false); - } - // endregion - - public Optional getStatus() { - return ofNullable(this.status); - } - - public List getUserExpectationsForArticle(User user, Article article) { - return this.expectations.stream() - .filter(execution -> execution.getType().equals(InjectExpectation.EXPECTATION_TYPE.ARTICLE)) - .filter(execution -> execution.getArticle().equals(article)) - .filter(execution -> execution.getAudience().getUsers().contains(user)) - .toList(); - } - - @JsonIgnore - public DryInject toDryInject(Dryrun run) { - DryInject dryInject = new DryInject(); - dryInject.setRun(run); - dryInject.setInject(this); - dryInject.setDate(computeInjectDate(run.getDate(), run.getSpeed())); - return dryInject; - } - - @JsonProperty("inject_communications_number") - public long getCommunicationsNumber() { - return this.getCommunications().size(); - } - - @JsonProperty("inject_communications_not_ack_number") - public long getCommunicationsNotAckNumber() { - return this.getCommunications().stream().filter(communication -> !communication.getAck()).count(); - } - - @JsonProperty("inject_sent_at") - public Instant getSentAt() { - if (this.getStatus().isPresent()) { - return this.getStatus().orElseThrow().getDate(); + // endregion + + public Optional getStatus() { + return ofNullable(this.status); + } + + public List getUserExpectationsForArticle(User user, Article article) { + return this.expectations.stream() + .filter(execution -> execution.getType().equals(InjectExpectation.EXPECTATION_TYPE.ARTICLE)) + .filter(execution -> execution.getArticle().equals(article)) + .filter(execution -> execution.getTeam().getUsers().contains(user)) + .toList(); + } + + @JsonIgnore + public DryInject toDryInject(Dryrun run) { + DryInject dryInject = new DryInject(); + dryInject.setRun(run); + dryInject.setInject(this); + dryInject.setDate(computeInjectDate(run.getDate(), run.getSpeed())); + return dryInject; + } + + @JsonProperty("inject_communications_number") + public long getCommunicationsNumber() { + return this.getCommunications().size(); + } + + @JsonProperty("inject_communications_not_ack_number") + public long getCommunicationsNotAckNumber() { + return this.getCommunications().stream().filter(communication -> !communication.getAck()).count(); + } + + @JsonProperty("inject_sent_at") + public Instant getSentAt() { + if (this.getStatus().isPresent()) { + return this.getStatus().orElseThrow().getDate(); + } + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !Base.class.isAssignableFrom(o.getClass())) { + return false; + } + Base base = (Base) o; + return id.equals(base.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id); } - return null; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || !Base.class.isAssignableFrom(o.getClass())) { - return false; - } - Base base = (Base) o; - return id.equals(base.getId()); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } } diff --git a/openex-model/src/main/java/io/openex/database/model/InjectExpectation.java b/openex-model/src/main/java/io/openex/database/model/InjectExpectation.java index 8969ec7b30..8aa6f04e92 100644 --- a/openex-model/src/main/java/io/openex/database/model/InjectExpectation.java +++ b/openex-model/src/main/java/io/openex/database/model/InjectExpectation.java @@ -104,10 +104,10 @@ public enum EXPECTATION_TYPE { @Setter @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "audience_id") + @JoinColumn(name = "team_id") @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("inject_expectation_audience") - private Audience audience; + @JsonProperty("inject_expectation_team") + private Team team; // endregion @ManyToOne(fetch = FetchType.LAZY) diff --git a/openex-model/src/main/java/io/openex/database/model/LessonsCategory.java b/openex-model/src/main/java/io/openex/database/model/LessonsCategory.java index 0bd6902d27..44224a4498 100644 --- a/openex-model/src/main/java/io/openex/database/model/LessonsCategory.java +++ b/openex-model/src/main/java/io/openex/database/model/LessonsCategory.java @@ -53,12 +53,12 @@ public class LessonsCategory implements Base { private int order; @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "lessons_categories_audiences", + @JoinTable(name = "lessons_categories_teams", joinColumns = @JoinColumn(name = "lessons_category_id"), - inverseJoinColumns = @JoinColumn(name = "audience_id")) + inverseJoinColumns = @JoinColumn(name = "team_id")) @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("lessons_category_audiences") - private List audiences = new ArrayList<>(); + @JsonProperty("lessons_category_teams") + private List teams = new ArrayList<>(); @OneToMany(mappedBy = "category", fetch = FetchType.LAZY) @JsonProperty("lessons_category_questions") @@ -68,7 +68,7 @@ public class LessonsCategory implements Base { // region transient @JsonProperty("lessons_category_users") public List getUsers() { - return getAudiences().stream().flatMap(audience -> audience.getUsers().stream().map(User::getId)).toList(); + return getTeams().stream().flatMap(team -> team.getUsers().stream().map(User::getId)).toList(); } // endregion @@ -129,12 +129,12 @@ public void setOrder(int order) { this.order = order; } - public List getAudiences() { - return audiences; + public List getTeams() { + return teams; } - public void setAudiences(List audiences) { - this.audiences = audiences; + public void setTeams(List teams) { + this.teams = teams; } public List getQuestions() { diff --git a/openex-model/src/main/java/io/openex/database/model/Organization.java b/openex-model/src/main/java/io/openex/database/model/Organization.java index 24d802339d..635ce915ac 100644 --- a/openex-model/src/main/java/io/openex/database/model/Organization.java +++ b/openex-model/src/main/java/io/openex/database/model/Organization.java @@ -60,10 +60,10 @@ public class Organization implements Base { private transient List injects = new ArrayList<>(); public void resolveInjects(Iterable injects) { this.injects = stream(injects.spliterator(), false) - .filter(inject -> inject.isAllAudiences() || inject.getAudiences().stream() - .anyMatch(audience -> getUsers().stream() - .flatMap(user -> user.getAudiences().stream()).toList() - .contains(audience))) + .filter(inject -> inject.isAllTeams() || inject.getTeams().stream() + .anyMatch(team -> getUsers().stream() + .flatMap(user -> user.getTeams().stream()).toList() + .contains(team))) .collect(Collectors.toList()); } diff --git a/openex-model/src/main/java/io/openex/database/model/Audience.java b/openex-model/src/main/java/io/openex/database/model/Team.java similarity index 57% rename from openex-model/src/main/java/io/openex/database/model/Audience.java rename to openex-model/src/main/java/io/openex/database/model/Team.java index d0f0a4ba02..60299d4450 100644 --- a/openex-model/src/main/java/io/openex/database/model/Audience.java +++ b/openex-model/src/main/java/io/openex/database/model/Team.java @@ -6,9 +6,13 @@ import io.openex.database.audit.ModelBaseListener; import io.openex.helper.MonoIdDeserializer; import io.openex.helper.MultiIdDeserializer; +import io.openex.helper.MultiModelDeserializer; +import lombok.Getter; +import lombok.Setter; import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; +import javax.validation.constraints.NotBlank; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -18,101 +22,112 @@ import static java.time.Instant.now; @Entity -@Table(name = "audiences") +@Table(name = "teams") @EntityListeners(ModelBaseListener.class) -public class Audience implements Base { +public class Team implements Base { @Id - @Column(name = "audience_id") + @Column(name = "team_id") @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") - @JsonProperty("audience_id") + @JsonProperty("team_id") private String id; - @Column(name = "audience_name") - @JsonProperty("audience_name") + @Setter + @Column(name = "team_name") + @NotBlank + @JsonProperty("team_name") private String name; - @Column(name = "audience_description") - @JsonProperty("audience_description") + @Setter + @Column(name = "team_description") + @JsonProperty("team_description") private String description; - @Column(name = "audience_enabled") - @JsonProperty("audience_enabled") - private boolean enabled = true; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "audience_exercise") - @JsonSerialize(using = MonoIdDeserializer.class) - @JsonProperty("audience_exercise") - private Exercise exercise; - - @Column(name = "audience_created_at") - @JsonProperty("audience_created_at") + @Setter + @Column(name = "team_created_at") + @JsonProperty("team_created_at") private Instant createdAt = now(); - @Column(name = "audience_updated_at") - @JsonProperty("audience_updated_at") + @Setter + @Column(name = "team_updated_at") + @JsonProperty("team_updated_at") private Instant updatedAt = now(); @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "audiences_tags", - joinColumns = @JoinColumn(name = "audience_id"), + @JoinTable(name = "teams_tags", + joinColumns = @JoinColumn(name = "team_id"), inverseJoinColumns = @JoinColumn(name = "tag_id")) @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("audience_tags") + @JsonProperty("team_tags") private List tags = new ArrayList<>(); + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_organization") + @JsonSerialize(using = MonoIdDeserializer.class) + @JsonProperty("team_organization") + private Organization organization; + @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "users_audiences", - joinColumns = @JoinColumn(name = "audience_id"), + @JoinTable(name = "users_teams", + joinColumns = @JoinColumn(name = "team_id"), inverseJoinColumns = @JoinColumn(name = "user_id")) @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("audience_users") + @JsonProperty("team_users") private List users = new ArrayList<>(); - @JsonProperty("audience_users_number") + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "exercises_teams", + joinColumns = @JoinColumn(name = "team_id"), + inverseJoinColumns = @JoinColumn(name = "exercise_id")) + @JsonSerialize(using = MultiIdDeserializer.class) + @JsonProperty("team_exercises") + private List exercises = new ArrayList<>(); + + @Getter + @Setter + @OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JsonProperty("team_exercises_users") + @JsonSerialize(using = MultiModelDeserializer.class) + private List exerciseTeamUsers = new ArrayList<>(); + + @JsonProperty("team_users_number") public long getUsersNumber() { return getUsers().size(); } // region transient - @JsonProperty("audience_injects") + @JsonProperty("team_injects") @JsonSerialize(using = MultiIdDeserializer.class) public List getInjects() { - Predicate selectedInject = inject -> inject.isAllAudiences() || inject.getAudiences().contains(this); - return getExercise().getInjects().stream().filter(selectedInject).distinct().toList(); + Predicate selectedInject = inject -> inject.isAllTeams() || inject.getTeams().contains(this); + return getExercises().stream().map(exercise -> exercise.getInjects().stream().filter(selectedInject).distinct().toList()).flatMap(List::stream).toList(); } - @JsonProperty("audience_injects_number") + @JsonProperty("team_injects_number") public long getInjectsNumber() { return getInjects().size(); } - @OneToMany(mappedBy = "audience", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "team", fetch = FetchType.LAZY) @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("audience_inject_expectations") + @JsonProperty("team_inject_expectations") private List injectExpectations = new ArrayList<>(); - @JsonProperty("audience_injects_expectations_number") + @JsonProperty("team_injects_expectations_number") public long getInjectExceptationsNumber() { return getInjectExpectations().size(); } - @JsonProperty("audience_injects_expectations_total_score") + @JsonProperty("team_injects_expectations_total_score") public long getInjectExceptationsTotalScore() { return getInjectExpectations().stream().mapToLong(InjectExpectation::getScore).sum(); } - @JsonProperty("audience_injects_expectations_total_expected_score") + @JsonProperty("team_injects_expectations_total_expected_score") public long getInjectExceptationsTotalExpectedScore() { return getInjectExpectations().stream().mapToLong(InjectExpectation::getExpectedScore).sum(); } - - @JsonIgnore - @Override - public boolean isUserHasAccess(User user) { - return exercise.isUserHasAccess(user); - } // endregion public String getId() { @@ -139,14 +154,6 @@ public void setDescription(String description) { this.description = description; } - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - public Instant getCreatedAt() { return createdAt; } @@ -171,6 +178,14 @@ public void setTags(List tags) { this.tags = tags; } + public Organization getOrganization() { + return organization; + } + + public void setOrganization(Organization organization) { + this.organization = organization; + } + public List getUsers() { return users; } @@ -179,12 +194,20 @@ public void setUsers(List users) { this.users = users; } - public Exercise getExercise() { - return exercise; + public List getExercises() { + return exercises; + } + + public void setExercises(List exercises) { + this.exercises = exercises; } - public void setExercise(Exercise exercise) { - this.exercise = exercise; + public List getExerciseTeamUsers() { + return exerciseTeamUsers; + } + + public void setExerciseTeamUsers(List exerciseTeamUsers) { + this.exerciseTeamUsers = exerciseTeamUsers; } public List getInjectExpectations() { @@ -195,13 +218,17 @@ public void setInjectExpectations(List injectExpectations) { this.injectExpectations = injectExpectations; } - @JsonProperty("audience_communications") + @JsonProperty("team_communications") public List getCommunications() { return getInjects().stream().flatMap(inject -> inject.getCommunications().stream()) .distinct() .toList(); } + public long getUsersNumberInExercise(Exercise exercise) { + return getExerciseTeamUsers().stream().filter(exerciseTeamUser -> exerciseTeamUser.getExercise().getId().equals(exercise.getId())).toList().size(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/openex-model/src/main/java/io/openex/database/model/User.java b/openex-model/src/main/java/io/openex/database/model/User.java index 5786c32316..61f34a47ef 100644 --- a/openex-model/src/main/java/io/openex/database/model/User.java +++ b/openex-model/src/main/java/io/openex/database/model/User.java @@ -129,6 +129,7 @@ public class User implements Base { private String city; @Setter + // @ManyToMany(fetch = FetchType.LAZY) @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "users_groups", joinColumns = @JoinColumn(name = "user_id"), @@ -139,12 +140,12 @@ public class User implements Base { @Setter @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "users_audiences", + @JoinTable(name = "users_teams", joinColumns = @JoinColumn(name = "user_id"), - inverseJoinColumns = @JoinColumn(name = "audience_id")) + inverseJoinColumns = @JoinColumn(name = "team_id")) @JsonSerialize(using = MultiIdDeserializer.class) - @JsonProperty("user_audiences") - private List audiences = new ArrayList<>(); + @JsonProperty("user_teams") + private List teams = new ArrayList<>(); @Setter @ManyToMany(fetch = FetchType.LAZY) @@ -186,8 +187,8 @@ public String getTheme() { public void resolveInjects(Iterable injects) { this.injects = stream(injects.spliterator(), false) - .filter(inject -> inject.isAllAudiences() || inject.getAudiences().stream() - .anyMatch(audience -> getAudiences().contains(audience))) + .filter(inject -> inject.isAllTeams() || inject.getTeams().stream() + .anyMatch(team -> getTeams().contains(team))) .collect(Collectors.toList()); } @@ -228,7 +229,7 @@ public boolean isManager() { @JsonProperty("user_is_player") public boolean isPlayer() { - return isAdmin() || isPlanner() || isObserver() || !getAudiences().isEmpty(); + return isAdmin() || isPlanner() || isObserver() || !getTeams().isEmpty(); } @JsonProperty("user_last_comcheck") diff --git a/openex-model/src/main/java/io/openex/database/repository/AudienceRepository.java b/openex-model/src/main/java/io/openex/database/repository/AudienceRepository.java deleted file mode 100644 index 1dd8ff0bc1..0000000000 --- a/openex-model/src/main/java/io/openex/database/repository/AudienceRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.openex.database.repository; - -import io.openex.database.model.Audience; -import javax.validation.constraints.NotNull; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AudienceRepository extends CrudRepository, JpaSpecificationExecutor { - - @NotNull - Optional findById(@NotNull String id); -} diff --git a/openex-model/src/main/java/io/openex/database/repository/ExerciseTeamUserRepository.java b/openex-model/src/main/java/io/openex/database/repository/ExerciseTeamUserRepository.java new file mode 100644 index 0000000000..5c3d92dab5 --- /dev/null +++ b/openex-model/src/main/java/io/openex/database/repository/ExerciseTeamUserRepository.java @@ -0,0 +1,37 @@ +package io.openex.database.repository; + +import io.openex.database.model.ExerciseTeamUser; +import io.openex.database.model.ExerciseTeamUserId; +import io.openex.database.model.InjectDocument; +import io.openex.database.model.InjectDocumentId; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Repository +public interface ExerciseTeamUserRepository extends CrudRepository, JpaSpecificationExecutor { + + @NotNull + Optional findById(@NotNull ExerciseTeamUserId id); + + @Modifying + @Query(value = "delete from exercises_teams_users i where i.user_id = :userId", nativeQuery = true) + void deleteUserFromAllReferences(@Param("userId") String userId); + + @Modifying + @Query(value = "delete from exercises_teams_users i where i.team_id = :teamId", nativeQuery = true) + void deleteTeamFromAllReferences(@Param("teamId") String teamId); + + @Modifying + @Query(value = "insert into exercises_teams_users (exercise_id, team_id, user_id) " + + "values (:exerciseId, :teamId, :userId)", nativeQuery = true) + void addExerciseTeamUser(@Param("exerciseId") String exerciseId, + @Param("teamId") String teamId, + @Param("userId") String userId); +} diff --git a/openex-model/src/main/java/io/openex/database/repository/InjectExpectationRepository.java b/openex-model/src/main/java/io/openex/database/repository/InjectExpectationRepository.java index db01958f4a..4102388344 100644 --- a/openex-model/src/main/java/io/openex/database/repository/InjectExpectationRepository.java +++ b/openex-model/src/main/java/io/openex/database/repository/InjectExpectationRepository.java @@ -29,14 +29,14 @@ List findAllForExerciseAndInject( ); @Query(value = "select i from InjectExpectation i where i.exercise.id = :exerciseId " + - "and i.type = 'CHALLENGE' and i.audience.id IN (:audienceIds)") + "and i.type = 'CHALLENGE' and i.team.id IN (:teamIds)") List findChallengeExpectations(@Param("exerciseId") String exerciseId, - @Param("audienceIds") List audienceIds); + @Param("teamIds") List teamIds); @Query(value = "select i from InjectExpectation i where i.exercise.id = :exerciseId " + - "and i.challenge.id = :challengeId and i.audience.id IN (:audienceIds)") + "and i.challenge.id = :challengeId and i.team.id IN (:teamIds)") List findChallengeExpectations(@Param("exerciseId") String exerciseId, - @Param("audienceIds") List audienceIds, + @Param("teamIds") List teamIds, @Param("challengeId") String challengeId); @Modifying diff --git a/openex-model/src/main/java/io/openex/database/repository/InjectRepository.java b/openex-model/src/main/java/io/openex/database/repository/InjectRepository.java index f025ae8f6b..01b30030fe 100644 --- a/openex-model/src/main/java/io/openex/database/repository/InjectRepository.java +++ b/openex-model/src/main/java/io/openex/database/repository/InjectRepository.java @@ -34,9 +34,9 @@ public interface InjectRepository extends CrudRepository, JpaSpe @Modifying @Query(value = "insert into injects (inject_id, inject_title, inject_description, inject_country, inject_city," + - "inject_type, inject_contract, inject_all_audiences, inject_enabled, inject_exercise, inject_depends_from_another, " + + "inject_type, inject_contract, inject_all_teams, inject_enabled, inject_exercise, inject_depends_from_another, " + "inject_depends_duration, inject_content) " + - "values (:id, :title, :description, :country, :city, :type, :contract, :allAudiences, :enabled, :exercise, :dependsOn, :dependsDuration, :content)", nativeQuery = true) + "values (:id, :title, :description, :country, :city, :type, :contract, :allTeams, :enabled, :exercise, :dependsOn, :dependsDuration, :content)", nativeQuery = true) void importSave(@Param("id") String id, @Param("title") String title, @Param("description") String description, @@ -44,7 +44,7 @@ void importSave(@Param("id") String id, @Param("city") String city, @Param("type") String type, @Param("contract") String contract, - @Param("allAudiences") boolean allAudiences, + @Param("allTeams") boolean allTeams, @Param("enabled") boolean enabled, @Param("exercise") String exerciseId, @Param("dependsOn") String dependsOn, @@ -56,8 +56,8 @@ void importSave(@Param("id") String id, void addTag(@Param("injectId") String injectId, @Param("tagId") String tagId); @Modifying - @Query(value = "insert into injects_audiences (inject_id, audience_id) values (:injectId, :audienceId)", nativeQuery = true) - void addAudience(@Param("injectId") String injectId, @Param("audienceId") String audienceId); + @Query(value = "insert into injects_teams (inject_id, team_id) values (:injectId, :teamId)", nativeQuery = true) + void addTeam(@Param("injectId") String injectId, @Param("teamId") String teamId); @Override @Query("select count(distinct i) from Inject i " + diff --git a/openex-model/src/main/java/io/openex/database/repository/TeamRepository.java b/openex-model/src/main/java/io/openex/database/repository/TeamRepository.java new file mode 100644 index 0000000000..77fb800de1 --- /dev/null +++ b/openex-model/src/main/java/io/openex/database/repository/TeamRepository.java @@ -0,0 +1,27 @@ +package io.openex.database.repository; + +import io.openex.database.model.Challenge; +import io.openex.database.model.Team; +import javax.validation.constraints.NotNull; + +import io.openex.database.model.User; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TeamRepository extends CrudRepository, JpaSpecificationExecutor { + + @NotNull + Optional findById(@NotNull String id); + + List findByNameIgnoreCase(String name); + + @Query("select team from Team team where team.organization is null or team.organization.id in :organizationIds") + List teamsAccessibleFromOrganizations(@Param("organizationIds") List organizationIds); +} diff --git a/openex-model/src/main/java/io/openex/database/repository/UserRepository.java b/openex-model/src/main/java/io/openex/database/repository/UserRepository.java index b0afe8715b..683564ef2a 100644 --- a/openex-model/src/main/java/io/openex/database/repository/UserRepository.java +++ b/openex-model/src/main/java/io/openex/database/repository/UserRepository.java @@ -29,8 +29,8 @@ public interface UserRepository extends CrudRepository, JpaSpecifi @Override @Query("select count(distinct u) from User u " + - "join u.audiences as audience " + - "join audience.exercise as e " + + "join u.teams as team " + + "join team.exercises as e " + "join e.grants as grant " + "join grant.group.users as user " + "where user.id = :userId and u.createdAt < :creationDate") diff --git a/openex-model/src/main/java/io/openex/database/specification/AudienceSpecification.java b/openex-model/src/main/java/io/openex/database/specification/AudienceSpecification.java deleted file mode 100644 index 3ed9c20106..0000000000 --- a/openex-model/src/main/java/io/openex/database/specification/AudienceSpecification.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.openex.database.specification; - -import io.openex.database.model.Audience; -import org.springframework.data.jpa.domain.Specification; - -public class AudienceSpecification { - - public static Specification fromExercise(String exerciseId) { - return (root, query, cb) -> cb.equal(root.get("exercise").get("id"), exerciseId); - } -}