diff --git a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/controller/SkillLevelController.java b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/controller/SkillLevelController.java index e8d92c8..1f2fa10 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/controller/SkillLevelController.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/controller/SkillLevelController.java @@ -3,6 +3,8 @@ import de.unistuttgart.iste.meitrex.common.user_handling.GlobalPermissionAccessValidator; import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser; import de.unistuttgart.iste.meitrex.generated.dto.SkillLevels; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillAllUsersStatsEntity; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillValueEntity; import de.unistuttgart.iste.meitrex.skilllevel_service.service.SkillLevelService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,7 +15,6 @@ import org.springframework.stereotype.Controller; import java.util.List; -import java.util.Set; import java.util.UUID; @Slf4j @@ -44,5 +45,14 @@ public List skillLevelForUserBySkillIds(@Argument final List return skilllevelService.getSkillLevelsForSkillIds(skillIds, userId); } + @QueryMapping(name = INTERNAL_NOAUTH_PREFIX + "skillsAllUsersStatsBySkillIds") + public List skillsAllUsersStatsBySkillIds(@Argument final List skillIds) { + return skilllevelService.getSkillsAllUsersStatsForSkillIds(skillIds); + } + + @QueryMapping(name = INTERNAL_NOAUTH_PREFIX + "skillValuesBySkillIds") + public List skillValuesBySkillIds(@Argument final List skillIds, @ContextValue final LoggedInUser currentUser) { + return skilllevelService.getSkillValuesForSkillIds(skillIds, currentUser.getId()); + } } diff --git a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/entity/SkillAllUsersStatsEntity.java b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/entity/SkillAllUsersStatsEntity.java new file mode 100644 index 0000000..cf11524 --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/entity/SkillAllUsersStatsEntity.java @@ -0,0 +1,25 @@ +package de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.UUID; +/** * Database entity which represents for every skill the skillStats for all users in the course. */ + +@Entity(name = "SkillAllUsersStats") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SkillAllUsersStatsEntity { + + @Id private UUID skillId; + + /** * Current sum of the skill values from all users in a course with this skill. */ + @Column(nullable = false) private float skillValueSum; + + /** * Current participant count of the skill level. */ + @Column(nullable = false) private int participantCount; + +} \ No newline at end of file diff --git a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/entity/SkillValueEntity.java b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/entity/SkillValueEntity.java new file mode 100644 index 0000000..6770f63 --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/entity/SkillValueEntity.java @@ -0,0 +1,22 @@ +package de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.UUID; +/** * Database entity which represents for every skill the skillValue in the course. */ + +@Entity(name = "SkillValue") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SkillValueEntity { + + @Id private UUID skillId; + + /** * Current skill value. Can range from 0 to 1. */ + @Column(nullable = false) private float skillValue; + +} \ No newline at end of file diff --git a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/repository/SkillAllUsersStatsRepository.java b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/repository/SkillAllUsersStatsRepository.java new file mode 100644 index 0000000..e25931b --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/repository/SkillAllUsersStatsRepository.java @@ -0,0 +1,9 @@ +package de.unistuttgart.iste.meitrex.skilllevel_service.persistence.repository; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillAllUsersStatsEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.UUID; + +@Repository +public interface SkillAllUsersStatsRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/repository/SkillValueRepository.java b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/repository/SkillValueRepository.java new file mode 100644 index 0000000..269746c --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/persistence/repository/SkillValueRepository.java @@ -0,0 +1,9 @@ +package de.unistuttgart.iste.meitrex.skilllevel_service.persistence.repository; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillValueEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.UUID; + +@Repository +public interface SkillValueRepository extends JpaRepository { +} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelService.java b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelService.java index 2118542..5ed20d3 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelService.java @@ -6,6 +6,7 @@ import de.unistuttgart.iste.meitrex.common.event.ItemResponse; import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.AllSkillLevelsEntity; import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillAbilityEntity; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillAllUsersStatsEntity; import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillLevelEntity; import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillsForCourse; import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.mapper.SkillLevelMapper; @@ -17,10 +18,13 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; import java.util.*; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillValueEntity; + @Service @Slf4j @RequiredArgsConstructor @@ -77,7 +81,92 @@ public List getSkillLevelsForCourse(UUID courseId, UUID userId) { */ public List getSkillLevelsForSkillIds(List skillIds, UUID userId) { return getSkillLevelEntitiesForSkillIds(skillIds, userId).stream().map(mapper::entityToDto).toList(); + } + + /** + * Returns the stats for the skills for all users from the course of the skills. + * + * @param skillIds List of skillIds + * @return List of SkillAllUsersStatsEntity that represents stats + * like skillValueSum and participantCount of the given skills + */ + public List getSkillsAllUsersStatsForSkillIds(List skillIds) { + List skillAllUsersStats = new ArrayList<>(); + for (UUID skillId : skillIds) { + skillAllUsersStats.add(getSkillAllUsersStatsForSkillId(skillId)); + } + return skillAllUsersStats; + } + + /** + * Returns the skill's stats for a given skillId. + * + * @param skillId skillId of skillLevels + * @return SkillValueAllUsersStatsEntity that represents stats + * like skillValue sum and participantCount of the given skills + */ + private SkillAllUsersStatsEntity getSkillAllUsersStatsForSkillId(UUID skillId) { + List skillLevels = skillLevelsRepository.findByIdSkillId(skillId); + List skillValues = new ArrayList<>(); + for (AllSkillLevelsEntity skillLevel : skillLevels) { + skillValues.add(getSkillValueForSkillLevel(skillLevel)); + } + float skillValueSum = 0f; + for (Float skillValue : skillValues) { + skillValueSum += skillValue; + } + SkillAllUsersStatsEntity skillAllUsersStats = new SkillAllUsersStatsEntity(); + skillAllUsersStats.setSkillId(skillId); + skillAllUsersStats.setParticipantCount(skillValues.size()); + if (skillValues.isEmpty()) { + skillAllUsersStats.setSkillValueSum(0f); + } else { + skillAllUsersStats.setSkillValueSum(skillValueSum); + } + return skillAllUsersStats; + } + /** + * Returns the skill value for a given skillLevel by combining all 6 skillLevelEntities. + * + * @param skillLevel skillLevel of a skill + * @return A float value that represents the skill value of a skill + */ + private Float getSkillValueForSkillLevel(AllSkillLevelsEntity skillLevel) { + List values = List.of( + skillLevel.getRemember().getValue(), + skillLevel.getUnderstand().getValue(), + skillLevel.getApply().getValue(), + skillLevel.getAnalyze().getValue(), + skillLevel.getEvaluate().getValue(), + skillLevel.getCreate().getValue() + ); + List nonZeroValues = values.stream().filter(v -> v > 0f).toList(); + if (nonZeroValues.isEmpty()) { + return 0f; + } else { + float sum = nonZeroValues.stream().reduce(0f, Float::sum); + return sum / nonZeroValues.size(); + } + } + + /** + * return the skill values for the given user and skills + * + * @param skillIds the ids of the skills + * @param userId the id of the user + * @return the skill values for the given user and skills + */ + public List getSkillValuesForSkillIds(List skillIds, UUID userId) { + List skillLevels = getSkillLevelEntitiesForSkillIds(skillIds, userId); + List skillValues = new ArrayList<>(); + for (AllSkillLevelsEntity skillLevel : skillLevels) { + SkillValueEntity entity = new SkillValueEntity(); + entity.setSkillId(skillLevel.getId().getSkillId()); + entity.setSkillValue(getSkillValueForSkillLevel(skillLevel)); + skillValues.add(entity); + } + return skillValues; } /** diff --git a/src/main/resources/graphql/service/schema.graphqls b/src/main/resources/graphql/service/schema.graphqls index dfe1e25..36f0bcf 100644 --- a/src/main/resources/graphql/service/schema.graphqls +++ b/src/main/resources/graphql/service/schema.graphqls @@ -36,6 +36,9 @@ type Query { """ _internal_noauth_skillLevelForUserBySkillIds(skillIds: [UUID!]!, userId: UUID!): [SkillLevels!]! + _internal_noauth_skillsAllUsersStatsBySkillIds(skillIds: [UUID!]!): [SkillAllUsersStats!]! + + _internal_noauth_skillValuesBySkillIds(skillIds: [UUID!]!): [SkillValue!]! } type Mutation { @@ -129,4 +132,19 @@ type SkillLevelLogItem { the probability of a correct response, that M-Elo predicts """ predictedCorrectness:Float! +} + +type SkillValue { + skillId: UUID! + + skillValue: Float! +} + +type SkillAllUsersStats { + + skillId: UUID! + + skillValueSum: Float! + + participantCount: Int! } \ No newline at end of file diff --git a/src/test/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelUsersStatsTest.java b/src/test/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelUsersStatsTest.java new file mode 100644 index 0000000..087210d --- /dev/null +++ b/src/test/java/de/unistuttgart/iste/meitrex/skilllevel_service/service/SkillLevelUsersStatsTest.java @@ -0,0 +1,155 @@ +package de.unistuttgart.iste.meitrex.skilllevel_service.service; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import de.unistuttgart.iste.meitrex.skilllevel_service.service.calculation.SkillLevelCalculator; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.AllSkillLevelsEntity; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillLevelEntity; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillValueEntity; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.entity.SkillAllUsersStatsEntity; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.mapper.SkillLevelMapper; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.repository.AllSkillLevelsRepository; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.repository.ItemDifficultyRepository; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.repository.SkillAbilityRepository; +import de.unistuttgart.iste.meitrex.skilllevel_service.persistence.repository.SkillsForCourseRepository; +import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher; +import de.unistuttgart.iste.meitrex.skilllevel_service.service.SkillLevelService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class SkillLevelUsersStatsTest { + + private final AllSkillLevelsRepository skillLevelsRepository = Mockito.mock(AllSkillLevelsRepository.class); + private final SkillLevelMapper skillLevelMapper = Mockito.mock(SkillLevelMapper.class); + private final SkillLevelCalculator skillLevelCalculator = Mockito.mock(SkillLevelCalculator.class); + private final SkillAbilityRepository skillAbilityRepository = Mockito.mock(SkillAbilityRepository.class); + private final ItemDifficultyRepository itemDifficultyRepository = Mockito.mock(ItemDifficultyRepository.class); + private final SkillsForCourseRepository skillsForCourseRepository = Mockito.mock(SkillsForCourseRepository.class); + private final TopicPublisher topicPublisher = Mockito.mock(TopicPublisher.class); + + private final SkillLevelService skillLevelService = new SkillLevelService( + skillLevelsRepository, + skillLevelMapper, + skillLevelCalculator, + skillAbilityRepository, + itemDifficultyRepository, + skillsForCourseRepository, + topicPublisher + ); + + @Test + void testGetSkillValuesForSkillIds() { + final UUID userId = UUID.randomUUID(); + final UUID skillId1 = UUID.randomUUID(); + final UUID skillId2 = UUID.randomUUID(); + final UUID skillId3 = UUID.randomUUID(); + final UUID skillId4 = UUID.randomUUID(); + List skillIds = List.of(skillId1, skillId2, skillId3, skillId4); + + AllSkillLevelsEntity skillLevel1 = createAllSkillLevelEntity(userId, skillId1, 1f, 1f, 1f, 1f, 1f, 1f); + AllSkillLevelsEntity skillLevel2 = createAllSkillLevelEntity(userId, skillId2, 0f, 0f, 0f, 0f, 0f, 0f); + AllSkillLevelsEntity skillLevel3 = createAllSkillLevelEntity(userId, skillId3, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f); + AllSkillLevelsEntity skillLevel4 = createAllSkillLevelEntity(userId, skillId4, 0f, 0f, 0f, 0.5f, 0f, 0f); + + List skillLevels = List.of(skillLevel1, skillLevel2, skillLevel3, skillLevel4); + + final List primaryKeys = skillIds.stream() + .map(x -> new AllSkillLevelsEntity.PrimaryKey(x, userId)) + .toList(); + when(skillLevelsRepository.findAllById(primaryKeys)) + .thenReturn(skillLevels); + + List skillValues = skillLevelService.getSkillValuesForSkillIds(skillIds, userId); + + assertThat(skillValues).hasSize(4); + assertThat(skillValues).extracting(SkillValueEntity::getSkillId, SkillValueEntity::getSkillValue) + .containsExactlyInAnyOrder( + tuple(skillId1, 1f), + tuple(skillId2, 0f), + tuple(skillId3, 0.55f), + tuple(skillId4, 0.5f) + ); + verify(skillLevelsRepository, times(1)).findAllById(primaryKeys); + } + + @Test + void testGetSkillsAllUsersStatsForSkillIds() { + final UUID user1Id = UUID.randomUUID(); + final UUID user2Id = UUID.randomUUID(); + final UUID user3Id = UUID.randomUUID(); + + final UUID skillId1 = UUID.randomUUID(); + final UUID skillId2 = UUID.randomUUID(); + final UUID skillId3 = UUID.randomUUID(); + List skillIds = List.of(skillId1, skillId2, skillId3); + + AllSkillLevelsEntity user1Skill1 = createAllSkillLevelEntity(user1Id, skillId1, 1f, 1f, 1f, 1f, 1f, 1f); + AllSkillLevelsEntity user2Skill1 = createAllSkillLevelEntity(user2Id, skillId1, 1f, 1f, 1f, 1f, 1f, 1f); + + AllSkillLevelsEntity user1Skill2 = createAllSkillLevelEntity(user1Id, skillId2, 0f, 0f, 0f, 0f, 0f, 0f); + AllSkillLevelsEntity user2Skill2 = createAllSkillLevelEntity(user2Id, skillId2, 0f, 0f, 0f, 0f, 0f, 0f); + + AllSkillLevelsEntity user1Skill3 = createAllSkillLevelEntity(user1Id, skillId3, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f); + AllSkillLevelsEntity user2Skill3 = createAllSkillLevelEntity(user2Id, skillId3, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1f); + AllSkillLevelsEntity user3Skill3 = createAllSkillLevelEntity(user3Id, skillId3, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f); + + when(skillLevelsRepository.findByIdSkillId(skillId1)) + .thenReturn(List.of(user1Skill1, user2Skill1)); + when(skillLevelsRepository.findByIdSkillId(skillId2)) + .thenReturn(List.of(user1Skill2, user2Skill2)); + when(skillLevelsRepository.findByIdSkillId(skillId3)) + .thenReturn(List.of(user1Skill3, user2Skill3, user3Skill3)); + + List skillAllUsersStats = skillLevelService.getSkillsAllUsersStatsForSkillIds(skillIds); + + assertThat(skillAllUsersStats).hasSize(3); + assertThat(skillAllUsersStats).extracting(SkillAllUsersStatsEntity::getSkillId, SkillAllUsersStatsEntity::getSkillValueSum, SkillAllUsersStatsEntity::getParticipantCount) + .containsExactlyInAnyOrder( + tuple(skillId1, 2f, 2), + tuple(skillId2, 0f, 2), + tuple(skillId3, 1.75f, 3) + ); + + verify(skillLevelsRepository, times(1)).findByIdSkillId(skillId1); + verify(skillLevelsRepository, times(1)).findByIdSkillId(skillId2); + verify(skillLevelsRepository, times(1)).findByIdSkillId(skillId3); + } + + /** + * Helper method to create a skill level entity with specified values + */ + private AllSkillLevelsEntity createAllSkillLevelEntity(UUID userId, UUID skillId, + float remember, float understand, float apply, + float analyze, float evaluate, float create) { + AllSkillLevelsEntity entity = new AllSkillLevelsEntity(); + AllSkillLevelsEntity.PrimaryKey pk = new AllSkillLevelsEntity.PrimaryKey(); + pk.setUserId(userId); + pk.setSkillId(skillId); + entity.setId(pk); + + entity.setRemember(createSkillLevelEntity(remember)); + entity.setUnderstand(createSkillLevelEntity(understand)); + entity.setApply(createSkillLevelEntity(apply)); + entity.setAnalyze(createSkillLevelEntity(analyze)); + entity.setEvaluate(createSkillLevelEntity(evaluate)); + entity.setCreate(createSkillLevelEntity(create)); + + return entity; + } + + /** + * Helper method to create a SkillLevelEntity with a specific value + */ + private SkillLevelEntity createSkillLevelEntity(float value) { + SkillLevelEntity entity = new SkillLevelEntity(value); + entity.setValue(value); + return entity; + } +}