From 3d2f40b5b187fe96121946e0acb051343403aae7 Mon Sep 17 00:00:00 2001 From: GODrums Date: Fri, 29 Nov 2024 16:27:46 +0100 Subject: [PATCH 01/22] Modularization of task scheduling --- .../leaderboard/LeaderboardTaskScheduler.java | 77 +++++++ .../leaderboard/SlackMessageService.java | 212 ++---------------- .../tasks/SlackWeeklyLeaderboardTask.java | 172 ++++++++++++++ 3 files changed, 265 insertions(+), 196 deletions(-) create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java new file mode 100644 index 00000000..76d9a31a --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java @@ -0,0 +1,77 @@ +package de.tum.in.www1.hephaestus.leaderboard; + +import de.tum.in.www1.hephaestus.leaderboard.tasks.SlackWeeklyLeaderboardTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.support.CronExpression; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Service; + +@Order(value = Ordered.LOWEST_PRECEDENCE) +@EnableScheduling +@Service +public class LeaderboardTaskScheduler { + + private static final Logger logger = LoggerFactory.getLogger(LeaderboardTaskScheduler.class); + + @Value("${hephaestus.leaderboard.notification.enabled}") + private boolean runScheduledMessage; + + @Value("${hephaestus.leaderboard.schedule.day}") + private String scheduledDay; + + @Value("${hephaestus.leaderboard.schedule.time}") + private String scheduledTime; + + @Autowired + private ThreadPoolTaskScheduler taskScheduler; + + @Autowired + private SlackWeeklyLeaderboardTask slackWeeklyLeaderboardTask; + + + @EventListener(ApplicationReadyEvent.class) + public void activateTaskScheduler() { + + var timeParts = scheduledTime.split(":"); + + // CRON for the end of every leaderboard cycle + String cron = String.format( + "0 %s %s ? * %s", + timeParts.length > 1 ? timeParts[1] : 0, + timeParts[0], + scheduledDay + ); + + if (!CronExpression.isValidExpression(cron)) { + logger.error("Invalid cron expression: " + cron); + return; + } + + scheduleSlackMessage(cron); + + } + + /** + * Schedule a Slack message to be sent at the end of every leaderboard cycle. + */ + private void scheduleSlackMessage(String cron) { + if (!runScheduledMessage) return; + + if (!slackWeeklyLeaderboardTask.testSlackConnection()) { + logger.error("Failed to schedule Slack message"); + return; + } + + logger.info("Scheduling Slack message to run with {}", cron); + taskScheduler.schedule(slackWeeklyLeaderboardTask, new CronTrigger(cron)); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java index 847a7a83..0ae6f8c5 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java @@ -1,8 +1,5 @@ package de.tum.in.www1.hephaestus.leaderboard; -import static com.slack.api.model.block.Blocks.*; -import static com.slack.api.model.block.composition.BlockCompositions.*; - import com.slack.api.bolt.App; import com.slack.api.methods.SlackApiException; import com.slack.api.methods.request.chat.ChatPostMessageRequest; @@ -11,59 +8,33 @@ import com.slack.api.model.User; import com.slack.api.model.block.LayoutBlock; import java.io.IOException; -import java.time.DayOfWeek; -import java.time.OffsetDateTime; -import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.IntStream; -import org.apache.commons.text.similarity.LevenshteinDistance; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.scheduling.support.CronExpression; -import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; -@Order(value = Ordered.LOWEST_PRECEDENCE) -@EnableScheduling @Service public class SlackMessageService { private static final Logger logger = LoggerFactory.getLogger(SlackMessageService.class); - @Value("${hephaestus.leaderboard.notification.channel-id}") - private String channelId; - - @Value("${hephaestus.leaderboard.notification.enabled}") - private boolean runScheduledMessage; - - @Value("${hephaestus.host-url:localhost:8080}") - private String hephaestusUrl; - - @Value("${hephaestus.leaderboard.schedule.day}") - private String scheduledDay; - - @Value("${hephaestus.leaderboard.schedule.time}") - private String scheduledTime; - @Autowired private App slackApp; - @Autowired - private LeaderboardService leaderboardService; - - @Autowired - private ThreadPoolTaskScheduler taskScheduler; + /** + * Gets all members of the Slack workspace. + * @return + */ + public List getAllMembers() { + try { + return slackApp.client().usersList(r -> r).getMembers(); + } catch (IOException | SlackApiException e) { + logger.error("Failed to get all members from Slack: " + e.getMessage()); + return new ArrayList<>(); + } + } /** * Sends a message to the specified Slack channel. @@ -88,39 +59,10 @@ public void sendMessage(String channelId, List blocks, String fallb } } - @EventListener(ApplicationReadyEvent.class) - public void activateTaskScheduler() { - if (!runScheduledMessage) { - return; - } - - var timeParts = scheduledTime.split(":"); - - String cron = String.format( - "0 %s %s ? * %s", - timeParts.length > 1 ? timeParts[1] : 0, - timeParts[0], - scheduledDay - ); - - if (!CronExpression.isValidExpression(cron)) { - logger.error("Invalid cron expression: " + cron); - return; - } - - logger.info("Scheduling Slack message to run with {}", cron); - taskScheduler.schedule(new SlackWeeklyLeaderboardTask(), new CronTrigger(cron)); - } - /** - * Test the Slack app initialization on application startup. + * Test if the Slack app is correctly initialized. */ - @EventListener(ApplicationReadyEvent.class) - public void run() { - if (!runScheduledMessage) { - logger.info("Slack scheduled messages are disabled, skipping Slack app init test."); - return; - } + public boolean initTest() { logger.info("Testing Slack app initialization..."); AuthTestResponse response; try { @@ -132,132 +74,10 @@ public void run() { } if (response.isOk()) { logger.info("Slack app is successfully initialized."); + return true; } else { logger.error("Failed to initialize Slack app: " + response.getError()); - } - } - - /** - * Task to send a weekly leaderboard message to the Slack channel. - * @see SlackMessageService#activateTaskScheduler() - */ - private class SlackWeeklyLeaderboardTask implements Runnable { - - /** - * Gets the Slack handles of the top 3 reviewers in the given time frame. - * @return - */ - private List getTop3SlackReviewers(OffsetDateTime after, OffsetDateTime before) { - var leaderboard = leaderboardService.createLeaderboard(after, before, Optional.empty()); - var top3 = leaderboard.subList(0, Math.min(3, leaderboard.size())); - logger.debug("Top 3 Users of the last week: " + top3.stream().map(e -> e.user().name()).toList()); - - List allSlackUsers; - try { - allSlackUsers = slackApp.client().usersList(r -> r).getMembers(); - } catch (SlackApiException | IOException e) { - logger.error("Failed to get Slack users: " + e.getMessage()); - return new ArrayList<>(); - } - - return top3.stream().map(mapToSlackUser(allSlackUsers)).filter(user -> user != null).toList(); - } - - private Function mapToSlackUser(List allSlackUsers) { - return entry -> { - var exactUser = allSlackUsers - .stream() - .filter( - user -> - user.getName().equals(entry.user().name()) || - (user.getProfile().getEmail() != null && - user.getProfile().getEmail().equals(entry.user().email())) - ) - .findFirst(); - if (exactUser.isPresent()) { - return exactUser.get(); - } - - // find through String edit distance - return allSlackUsers - .stream() - .min((a, b) -> - Integer.compare( - LevenshteinDistance.getDefaultInstance().apply(entry.user().name(), a.getName()), - LevenshteinDistance.getDefaultInstance().apply(entry.user().name(), b.getName()) - ) - ) - .orElse(null); - }; - } - - private String formatDateForURL(OffsetDateTime date) { - return date.format(java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME).replace("+", "%2B"); - } - - @Override - public void run() { - // get date in unix format - long currentDate = OffsetDateTime.now().toEpochSecond(); - // Calculate the the last leaderboard schedule - String[] timeParts = scheduledTime.split(":"); - OffsetDateTime before = OffsetDateTime.now() - .with(TemporalAdjusters.previousOrSame(DayOfWeek.of(Integer.parseInt(scheduledDay)))) - .withHour(Integer.parseInt(timeParts[0])) - .withMinute(timeParts.length > 0 ? Integer.parseInt(timeParts[1]) : 0) - .withSecond(0) - .withNano(0); - OffsetDateTime after = before.minusWeeks(1); - - var top3reviewers = getTop3SlackReviewers(after, before); - - logger.info("Sending scheduled message to Slack channel..."); - - List blocks = asBlocks( - header(header -> - header.text(plainText(pt -> pt.text(":newspaper: Reviews of the last week :newspaper:"))) - ), - context(context -> - context.elements( - List.of( - markdownText( - " | " + hephaestusUrl - ) - ) - ) - ), - divider(), - section(section -> - section.text( - markdownText( - "Another *review leaderboard* has concluded. You can check out your placement <" + - hephaestusUrl + - "?after=" + - formatDateForURL(after) + - "&before=" + - formatDateForURL(before) + - "|here>." - ) - ) - ), - section(section -> section.text(markdownText("Special thanks to our top 3 reviewers of last week:"))), - section(section -> - section.text( - markdownText( - IntStream.range(0, top3reviewers.size()) - .mapToObj(i -> ((i + 1) + ". <@" + top3reviewers.get(i).getId() + ">")) - .reduce((a, b) -> a + "\n" + b) - .orElse("") - ) - ) - ), - section(section -> section.text(markdownText("Happy coding and reviewing! :rocket:"))) - ); - try { - sendMessage(channelId, blocks, "Reviews of the last week"); - } catch (IOException | SlackApiException e) { - logger.error("Failed to send scheduled message to Slack channel: " + e.getMessage()); - } + return false; } } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java new file mode 100644 index 00000000..9315f006 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java @@ -0,0 +1,172 @@ +package de.tum.in.www1.hephaestus.leaderboard.tasks; + +import static com.slack.api.model.block.Blocks.*; +import static com.slack.api.model.block.composition.BlockCompositions.*; + +import com.slack.api.methods.SlackApiException; +import com.slack.api.model.User; +import com.slack.api.model.block.LayoutBlock; +import de.tum.in.www1.hephaestus.leaderboard.LeaderboardEntryDTO; +import de.tum.in.www1.hephaestus.leaderboard.LeaderboardService; +import de.tum.in.www1.hephaestus.leaderboard.SlackMessageService; +import java.io.IOException; +import java.time.DayOfWeek; +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.IntStream; +import org.apache.commons.text.similarity.LevenshteinDistance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Task to send a weekly leaderboard message to the Slack channel. + * @see SlackMessageService#activateTaskScheduler() + */ +@Component +public class SlackWeeklyLeaderboardTask implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(SlackWeeklyLeaderboardTask.class); + + @Value("${hephaestus.leaderboard.notification.channel-id}") + private String channelId; + + @Value("${hephaestus.leaderboard.notification.enabled}") + private boolean runScheduledMessage; + + @Value("${hephaestus.host-url:localhost:8080}") + private String hephaestusUrl; + + @Value("${hephaestus.leaderboard.schedule.day}") + private String scheduledDay; + + @Value("${hephaestus.leaderboard.schedule.time}") + private String scheduledTime; + + @Autowired + private SlackMessageService slackMessageService; + + @Autowired + private LeaderboardService leaderboardService; + + /** + * Test the Slack connection. + * @return + */ + public boolean testSlackConnection() { + return runScheduledMessage && slackMessageService.initTest(); + } + + /** + * Gets the Slack handles of the top 3 reviewers in the given time frame. + * @return + */ + private List getTop3SlackReviewers(OffsetDateTime after, OffsetDateTime before) { + var leaderboard = leaderboardService.createLeaderboard(after, before, Optional.empty()); + var top3 = leaderboard.subList(0, Math.min(3, leaderboard.size())); + logger.debug("Top 3 Users of the last week: " + top3.stream().map(e -> e.user().name()).toList()); + + List allSlackUsers = slackMessageService.getAllMembers(); + + return top3.stream().map(mapToSlackUser(allSlackUsers)).filter(user -> user != null).toList(); + } + + private Function mapToSlackUser(List allSlackUsers) { + return entry -> { + var exactUser = allSlackUsers + .stream() + .filter( + user -> + user.getName().equals(entry.user().name()) || + (user.getProfile().getEmail() != null && + user.getProfile().getEmail().equals(entry.user().email())) + ) + .findFirst(); + if (exactUser.isPresent()) { + return exactUser.get(); + } + + // find through String edit distance + return allSlackUsers + .stream() + .min((a, b) -> + Integer.compare( + LevenshteinDistance.getDefaultInstance().apply(entry.user().name(), a.getName()), + LevenshteinDistance.getDefaultInstance().apply(entry.user().name(), b.getName()) + ) + ) + .orElse(null); + }; + } + + private String formatDateForURL(OffsetDateTime date) { + return date.format(java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME).replace("+", "%2B"); + } + + @Override + public void run() { + // get date in unix format + long currentDate = OffsetDateTime.now().toEpochSecond(); + // Calculate the the last leaderboard schedule + String[] timeParts = scheduledTime.split(":"); + OffsetDateTime before = OffsetDateTime.now() + .with(TemporalAdjusters.previousOrSame(DayOfWeek.of(Integer.parseInt(scheduledDay)))) + .withHour(Integer.parseInt(timeParts[0])) + .withMinute(timeParts.length > 0 ? Integer.parseInt(timeParts[1]) : 0) + .withSecond(0) + .withNano(0); + OffsetDateTime after = before.minusWeeks(1); + + var top3reviewers = getTop3SlackReviewers(after, before); + + logger.info("Sending scheduled message to Slack channel..."); + + List blocks = asBlocks( + header(header -> header.text(plainText(pt -> pt.text(":newspaper: Reviews of the last week :newspaper:")))), + context(context -> + context.elements( + List.of( + markdownText( + " | " + hephaestusUrl + ) + ) + ) + ), + divider(), + section(section -> + section.text( + markdownText( + "Another *review leaderboard* has concluded. You can check out your placement <" + + hephaestusUrl + + "?after=" + + formatDateForURL(after) + + "&before=" + + formatDateForURL(before) + + "|here>." + ) + ) + ), + section(section -> section.text(markdownText("Special thanks to our top 3 reviewers of last week:"))), + section(section -> + section.text( + markdownText( + IntStream.range(0, top3reviewers.size()) + .mapToObj(i -> ((i + 1) + ". <@" + top3reviewers.get(i).getId() + ">")) + .reduce((a, b) -> a + "\n" + b) + .orElse("") + ) + ) + ), + section(section -> section.text(markdownText("Happy coding and reviewing! :rocket:"))) + ); + try { + slackMessageService.sendMessage(channelId, blocks, "Reviews of the last week"); + } catch (IOException | SlackApiException e) { + logger.error("Failed to send scheduled message to Slack channel: " + e.getMessage()); + } + } +} From dda9e42eb0787ff76ddbd3718a6ceb760a1742b7 Mon Sep 17 00:00:00 2001 From: GODrums Date: Fri, 29 Nov 2024 16:51:33 +0100 Subject: [PATCH 02/22] Improve inline documentation --- .../hephaestus/leaderboard/LeaderboardTaskScheduler.java | 4 ++++ .../www1/hephaestus/leaderboard/SlackMessageService.java | 7 ++++++- .../leaderboard/tasks/SlackWeeklyLeaderboardTask.java | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java index 76d9a31a..4577da5c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java @@ -15,6 +15,10 @@ import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Service; +/** + * Schedules tasks to run at the end of every leaderboard cycle. + * @see SlackWeeklyLeaderboardTask + */ @Order(value = Ordered.LOWEST_PRECEDENCE) @EnableScheduling @Service diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java index 0ae6f8c5..48741894 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/SlackMessageService.java @@ -15,6 +15,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +/** + * Light wrapper around the Slack App to send messages to the Slack workspace. + * @implNote Use the exposed method to test the Slack connection beforehand. + */ @Service public class SlackMessageService { @@ -60,7 +64,8 @@ public void sendMessage(String channelId, List blocks, String fallb } /** - * Test if the Slack app is correctly initialized. + * Tests if the Slack app is correctly initialized and has access to the workspace. + * Does not guarantee that the app has the necessary permissions to send messages. */ public boolean initTest() { logger.info("Testing Slack app initialization..."); diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java index 9315f006..0c30e2f5 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/SlackWeeklyLeaderboardTask.java @@ -26,7 +26,7 @@ /** * Task to send a weekly leaderboard message to the Slack channel. - * @see SlackMessageService#activateTaskScheduler() + * @see SlackMessageService */ @Component public class SlackWeeklyLeaderboardTask implements Runnable { @@ -55,7 +55,7 @@ public class SlackWeeklyLeaderboardTask implements Runnable { /** * Test the Slack connection. - * @return + * @return {@code true} if the connection is valid, {@code false} otherwise. */ public boolean testSlackConnection() { return runScheduledMessage && slackMessageService.initTest(); From 07b0c5c314f67ad7f02373e0ea68c5b7a5848db1 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sat, 30 Nov 2024 12:41:48 +0100 Subject: [PATCH 03/22] Add league points calculation and new task for leaderboard --- .../hephaestus/gitprovider/user/User.java | 3 + .../LeaguePointsCalculationService.java | 61 +++++++++++++++++ .../tasks/LeaguePointsUpdateTask.java | 68 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java index c5b1b5fe..2301347f 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/User.java @@ -98,6 +98,9 @@ public class User extends BaseGitServiceEntity { @ToString.Exclude private Set reviewComments = new HashSet<>(); + // Current ranking points for the leaderboard leagues + private int leaguePoints; + public enum Type { USER, ORGANIZATION, BOT } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java new file mode 100644 index 00000000..f20c61a4 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -0,0 +1,61 @@ +package de.tum.in.www1.hephaestus.leaderboard; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class LeaguePointsCalculationService { + private final Logger logger = LoggerFactory.getLogger(LeaguePointsCalculationService.class); + + public int calculateNewPoints(int oldPoints, LeaderboardEntryDTO entry) { + // Base decay - the higher the points, the more you lose + int decay = calculateDecay(oldPoints); + + // Activity bonus based on leaderboard score + int activityBonus = calculateActivityBonus(entry.score()); + + // Additional bonus for review diversity + int diversityBonus = calculateDiversityBonus( + entry.numberOfApprovals(), + entry.numberOfChangeRequests(), + entry.numberOfComments(), + entry.numberOfCodeComments() + ); + + // Calculate final point change + int pointChange = activityBonus + diversityBonus - decay; + + // Apply minimum change to prevent extreme swings + int newPoints = Math.max(0, oldPoints + pointChange); + + logger.info("Points calculation: old={}, decay={}, activity={}, diversity={}, new={}", + oldPoints, decay, activityBonus, diversityBonus, newPoints); + + return newPoints; + } + + private int calculateDecay(int currentPoints) { + // 5% decay of current points, minimum 10 points if they have any points + return currentPoints > 0 ? Math.max(10, (int)(currentPoints * 0.05)) : 0; + } + + private int calculateActivityBonus(int score) { + // Convert leaderboard score directly to points with diminishing returns + return (int)(Math.sqrt(score) * 10); + } + + private int calculateDiversityBonus(int approvals, int changes, int comments, int codeComments) { + // Reward diverse review activity + int totalInteractions = approvals + changes + comments; + if (totalInteractions == 0) return 0; + + // Calculate how evenly distributed the review types are + double distribution = (double)(Math.min(approvals, Math.min(changes, comments))) / Math.max(approvals, Math.max(changes, comments)); + + // Bonus for code comments + int codeCommentBonus = Math.min(50, codeComments * 2); + + return (int)(distribution * 30) + codeCommentBonus; + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java new file mode 100644 index 00000000..0f4e9c8c --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java @@ -0,0 +1,68 @@ +package de.tum.in.www1.hephaestus.leaderboard.tasks; + +import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository; +import de.tum.in.www1.hephaestus.leaderboard.LeaderboardEntryDTO; +import de.tum.in.www1.hephaestus.leaderboard.LeaderboardService; +import de.tum.in.www1.hephaestus.leaderboard.LeaguePointsCalculationService; +import java.time.DayOfWeek; +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAdjusters; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class LeaguePointsUpdateTask implements Runnable { + + @Value("${hephaestus.leaderboard.schedule.day}") + private String scheduledDay; + + @Value("${hephaestus.leaderboard.schedule.time}") + private String scheduledTime; + + @Autowired + private UserRepository userRepository; + @Autowired + private LeaderboardService leaderboardService; + @Autowired + private LeaguePointsCalculationService leaguePointsCalculationService; + + @Override + public void run() { + List leaderboard = getLatestLeaderboard(); + leaderboard.forEach(updateLeaderboardEntry()); + } + + /** + * Returns a consumer that updates the ranking points of a user based on its leaderboard entry. + * @return + */ + private Consumer updateLeaderboardEntry() { + return entry -> { + var user = userRepository.findByLogin(entry.user().login()).orElseThrow(); + int newPoints = leaguePointsCalculationService.calculateNewPoints(user.getLeaguePoints(), entry); + user.setLeaguePoints(newPoints); + userRepository.save(user); + }; + } + + /** + * Returns the most recently completed leaderboard + * @return + */ + private List getLatestLeaderboard() { + String[] timeParts = scheduledTime.split(":"); + OffsetDateTime before = OffsetDateTime.now() + .with(TemporalAdjusters.previousOrSame(DayOfWeek.of(Integer.parseInt(scheduledDay)))) + .withHour(Integer.parseInt(timeParts[0])) + .withMinute(timeParts.length > 0 ? Integer.parseInt(timeParts[1]) : 0) + .withSecond(0) + .withNano(0); + OffsetDateTime after = before.minusWeeks(1); + return leaderboardService.createLeaderboard(after, before, Optional.empty()); + } + +} From e75b2550faf5260c8580506938f5c119b626f44e Mon Sep 17 00:00:00 2001 From: GODrums Date: Sat, 30 Nov 2024 12:44:58 +0100 Subject: [PATCH 04/22] Liquibase changelog --- .../resources/db/changelog/1732966971482_changelog.xml | 10 ++++++++++ .../src/main/resources/db/master.xml | 1 + 2 files changed, 11 insertions(+) create mode 100644 server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml diff --git a/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml b/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml new file mode 100644 index 00000000..ba965f5e --- /dev/null +++ b/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/server/application-server/src/main/resources/db/master.xml b/server/application-server/src/main/resources/db/master.xml index 23545da8..5e23a6ef 100644 --- a/server/application-server/src/main/resources/db/master.xml +++ b/server/application-server/src/main/resources/db/master.xml @@ -8,4 +8,5 @@ + From e07bc898f7ab3796a957034ddfdc2d655de20304 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sun, 1 Dec 2024 23:33:32 +0100 Subject: [PATCH 05/22] Schedule point calculation --- .../gitprovider/user/UserRepository.java | 8 ++ .../leaderboard/LeaderboardTaskScheduler.java | 12 ++- .../LeaguePointsCalculationService.java | 98 ++++++++++++------- .../tasks/LeaguePointsUpdateTask.java | 6 +- .../db/changelog/1732966971482_changelog.xml | 2 +- 5 files changed, 87 insertions(+), 39 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java index 72567beb..3da3833f 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserRepository.java @@ -17,6 +17,14 @@ public interface UserRepository extends JpaRepository { """) Optional findByLogin(@Param("login") String login); + @Query(""" + SELECT u + FROM User u + LEFT JOIN FETCH u.mergedPullRequests + WHERE u.login = :login + """) + Optional findByLoginWithEagerMergedPullRequests(@Param("login") String login); + @Query(""" SELECT u FROM User u diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java index 4577da5c..5e1c4ecb 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java @@ -1,5 +1,6 @@ package de.tum.in.www1.hephaestus.leaderboard; +import de.tum.in.www1.hephaestus.leaderboard.tasks.LeaguePointsUpdateTask; import de.tum.in.www1.hephaestus.leaderboard.tasks.SlackWeeklyLeaderboardTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +41,8 @@ public class LeaderboardTaskScheduler { @Autowired private SlackWeeklyLeaderboardTask slackWeeklyLeaderboardTask; + @Autowired + private LeaguePointsUpdateTask leaguePointsUpdateTask; @EventListener(ApplicationReadyEvent.class) @@ -61,7 +64,7 @@ public void activateTaskScheduler() { } scheduleSlackMessage(cron); - + scheduleLeaguePointsUpdate(cron); } /** @@ -78,4 +81,11 @@ private void scheduleSlackMessage(String cron) { logger.info("Scheduling Slack message to run with {}", cron); taskScheduler.schedule(slackWeeklyLeaderboardTask, new CronTrigger(cron)); } + + private void scheduleLeaguePointsUpdate(String cron) { + // if (!runScheduledMessage) return; + + logger.info("Scheduling league points update to run with {}", cron); + taskScheduler.schedule(leaguePointsUpdateTask, new CronTrigger(cron)); + } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java index f20c61a4..72af2518 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -1,5 +1,8 @@ package de.tum.in.www1.hephaestus.leaderboard; +import de.tum.in.www1.hephaestus.gitprovider.pullrequest.PullRequest; +import de.tum.in.www1.hephaestus.gitprovider.user.User; +import java.time.OffsetDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -8,54 +11,79 @@ public class LeaguePointsCalculationService { private final Logger logger = LoggerFactory.getLogger(LeaguePointsCalculationService.class); - public int calculateNewPoints(int oldPoints, LeaderboardEntryDTO entry) { - // Base decay - the higher the points, the more you lose - int decay = calculateDecay(oldPoints); - - // Activity bonus based on leaderboard score - int activityBonus = calculateActivityBonus(entry.score()); - - // Additional bonus for review diversity - int diversityBonus = calculateDiversityBonus( - entry.numberOfApprovals(), - entry.numberOfChangeRequests(), - entry.numberOfComments(), - entry.numberOfCodeComments() - ); - + public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { + // Initialize new players with 1000 points + if (user.getLeaguePoints() == 0) { + user.setLeaguePoints(1000); + } + + int oldPoints = user.getLeaguePoints(); + + double kFactor = getKFactor(user); + // Base decay + int decay = calculateDecay(user.getLeaguePoints()); + // Bonus based on leaderboard score + int performanceBonus = calculatePerformanceBonus(entry.score()); + // Additional bonus for placements + int placementBonus = calculatePlacementBonus(entry.rank()); // Calculate final point change - int pointChange = activityBonus + diversityBonus - decay; - + int pointChange = (int) (kFactor * (performanceBonus - decay)); // Apply minimum change to prevent extreme swings int newPoints = Math.max(0, oldPoints + pointChange); - logger.info("Points calculation: old={}, decay={}, activity={}, diversity={}, new={}", - oldPoints, decay, activityBonus, diversityBonus, newPoints); - + logger.info("Points calculation: old={}, k={} decay={}, performanceBonus={}, placement={}, pointchange={}, new={}", + oldPoints, kFactor, decay, performanceBonus, placementBonus, pointChange, newPoints); + return newPoints; } + + /** + * Calculate the K factor for the user based on their current points and if they are a new player. + * @param user + * @return + * @see Wikipedia: Most accurate K-factor + */ + private double getKFactor(User user) { + if (isNewPlayer(user)) { + return 2.0; + } else if (user.getLeaguePoints() < 1400) { + return 1.5; + } else if (user.getLeaguePoints() < 1800) { + return 1.2; + } else { + return 1.1; + } + } + + /** + * Check if the user's earliest merged pull request is within the last 30 days. + * @param user + * @return + */ + private boolean isNewPlayer(User user) { + return user.getMergedPullRequests().stream() + .filter(PullRequest::isMerged) + .map(PullRequest::getMergedAt) + .anyMatch(date -> date.isAfter(OffsetDateTime.now().minusDays(30))); + } + /** + * Calculate the base decay in points based on the current points. + * @param currentPoints + * @return + */ private int calculateDecay(int currentPoints) { - // 5% decay of current points, minimum 10 points if they have any points + // 10% decay of current points, minimum 10 points if they have any points return currentPoints > 0 ? Math.max(10, (int)(currentPoints * 0.05)) : 0; } - private int calculateActivityBonus(int score) { + private int calculatePerformanceBonus(int score) { // Convert leaderboard score directly to points with diminishing returns return (int)(Math.sqrt(score) * 10); } - - private int calculateDiversityBonus(int approvals, int changes, int comments, int codeComments) { - // Reward diverse review activity - int totalInteractions = approvals + changes + comments; - if (totalInteractions == 0) return 0; - - // Calculate how evenly distributed the review types are - double distribution = (double)(Math.min(approvals, Math.min(changes, comments))) / Math.max(approvals, Math.max(changes, comments)); - - // Bonus for code comments - int codeCommentBonus = Math.min(50, codeComments * 2); - - return (int)(distribution * 30) + codeCommentBonus; + + private int calculatePlacementBonus(int placement) { + // Bonus for top 3 placements + return placement <= 3 ? 20 * (4 - placement) : 0; } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java index 0f4e9c8c..41c3a69e 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java @@ -4,6 +4,7 @@ import de.tum.in.www1.hephaestus.leaderboard.LeaderboardEntryDTO; import de.tum.in.www1.hephaestus.leaderboard.LeaderboardService; import de.tum.in.www1.hephaestus.leaderboard.LeaguePointsCalculationService; +import jakarta.transaction.Transactional; import java.time.DayOfWeek; import java.time.OffsetDateTime; import java.time.temporal.TemporalAdjusters; @@ -31,6 +32,7 @@ public class LeaguePointsUpdateTask implements Runnable { private LeaguePointsCalculationService leaguePointsCalculationService; @Override + @Transactional public void run() { List leaderboard = getLatestLeaderboard(); leaderboard.forEach(updateLeaderboardEntry()); @@ -42,8 +44,8 @@ public void run() { */ private Consumer updateLeaderboardEntry() { return entry -> { - var user = userRepository.findByLogin(entry.user().login()).orElseThrow(); - int newPoints = leaguePointsCalculationService.calculateNewPoints(user.getLeaguePoints(), entry); + var user = userRepository.findByLoginWithEagerMergedPullRequests(entry.user().login()).orElseThrow(); + int newPoints = leaguePointsCalculationService.calculateNewPoints(user, entry); user.setLeaguePoints(newPoints); userRepository.save(user); }; diff --git a/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml b/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml index ba965f5e..215a8d73 100644 --- a/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml +++ b/server/application-server/src/main/resources/db/changelog/1732966971482_changelog.xml @@ -2,7 +2,7 @@ - + From 4301a087b455668429692f5dc117941e25f0515b Mon Sep 17 00:00:00 2001 From: GODrums Date: Mon, 2 Dec 2024 00:20:32 +0100 Subject: [PATCH 06/22] Introduce PlacementBonus --- .../hephaestus/leaderboard/LeaguePointsCalculationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java index 72af2518..89b212b4 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -27,7 +27,7 @@ public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { // Additional bonus for placements int placementBonus = calculatePlacementBonus(entry.rank()); // Calculate final point change - int pointChange = (int) (kFactor * (performanceBonus - decay)); + int pointChange = (int) (kFactor * (performanceBonus + placementBonus - decay)); // Apply minimum change to prevent extreme swings int newPoints = Math.max(0, oldPoints + pointChange); From 0e8c58a57259a8d2ea40560d3fed4c4e5fd13e82 Mon Sep 17 00:00:00 2001 From: GODrums Date: Mon, 2 Dec 2024 16:04:41 +0100 Subject: [PATCH 07/22] Improve JavaDocs --- .../hephaestus/leaderboard/LeaderboardTaskScheduler.java | 3 +-- .../leaderboard/LeaguePointsCalculationService.java | 2 +- .../leaderboard/tasks/LeaguePointsUpdateTask.java | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java index 00d7e0a9..f24e9495 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaderboardTaskScheduler.java @@ -41,6 +41,7 @@ public class LeaderboardTaskScheduler { @Autowired private SlackWeeklyLeaderboardTask slackWeeklyLeaderboardTask; + @Autowired private LeaguePointsUpdateTask leaguePointsUpdateTask; @@ -83,8 +84,6 @@ private void scheduleSlackMessage(String cron) { } private void scheduleLeaguePointsUpdate(String cron) { - // if (!runScheduledMessage) return; - logger.info("Scheduling league points update to run with {}", cron); taskScheduler.schedule(leaguePointsUpdateTask, new CronTrigger(cron)); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java index 89b212b4..eaa82605 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -73,7 +73,7 @@ private boolean isNewPlayer(User user) { * @return */ private int calculateDecay(int currentPoints) { - // 10% decay of current points, minimum 10 points if they have any points + // 5% decay of current points, minimum 10 points if they have any points return currentPoints > 0 ? Math.max(10, (int)(currentPoints * 0.05)) : 0; } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java index 41c3a69e..c9a4a48d 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/tasks/LeaguePointsUpdateTask.java @@ -39,8 +39,8 @@ public void run() { } /** - * Returns a consumer that updates the ranking points of a user based on its leaderboard entry. - * @return + * Update ranking points of a user based on its leaderboard entry. + * @return {@code Consumer} that updates {@code leaguePoints} based on its leaderboard entry. */ private Consumer updateLeaderboardEntry() { return entry -> { @@ -52,8 +52,8 @@ private Consumer updateLeaderboardEntry() { } /** - * Returns the most recently completed leaderboard - * @return + * Retrieves the latest leaderboard based on the scheduled time of the environment. + * @return List of {@code LeaderboardEntryDTO} representing the latest leaderboard */ private List getLatestLeaderboard() { String[] timeParts = scheduledTime.split(":"); From 5409312fe679aca127092aafb110b540564ed7c2 Mon Sep 17 00:00:00 2001 From: GODrums Date: Mon, 2 Dec 2024 16:11:32 +0100 Subject: [PATCH 08/22] Add more JavaDocs --- .../leaderboard/LeaguePointsCalculationService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java index eaa82605..9a8abe08 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -31,7 +31,7 @@ public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { // Apply minimum change to prevent extreme swings int newPoints = Math.max(0, oldPoints + pointChange); - logger.info("Points calculation: old={}, k={} decay={}, performanceBonus={}, placement={}, pointchange={}, new={}", + logger.info("Points calculation: old={}, k={}, decay={}, performanceBonus={}, placement={}, pointchange={}, new={}", oldPoints, kFactor, decay, performanceBonus, placementBonus, pointChange, newPoints); return newPoints; @@ -40,7 +40,7 @@ public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { /** * Calculate the K factor for the user based on their current points and if they are a new player. * @param user - * @return + * @return K factor * @see Wikipedia: Most accurate K-factor */ private double getKFactor(User user) { @@ -58,7 +58,7 @@ private double getKFactor(User user) { /** * Check if the user's earliest merged pull request is within the last 30 days. * @param user - * @return + * @return true if the pull request is within the last 30 days */ private boolean isNewPlayer(User user) { return user.getMergedPullRequests().stream() @@ -70,7 +70,7 @@ private boolean isNewPlayer(User user) { /** * Calculate the base decay in points based on the current points. * @param currentPoints - * @return + * @return Amount of decay points */ private int calculateDecay(int currentPoints) { // 5% decay of current points, minimum 10 points if they have any points From e3e1212874ffd7036b77a4b66fd384ddc7c36c66 Mon Sep 17 00:00:00 2001 From: Armin Stanitzok Date: Tue, 3 Dec 2024 13:49:26 +0100 Subject: [PATCH 09/22] fix: new player condition --- .../hephaestus/leaderboard/LeaguePointsCalculationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java index 9a8abe08..bae5d9e8 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -64,7 +64,7 @@ private boolean isNewPlayer(User user) { return user.getMergedPullRequests().stream() .filter(PullRequest::isMerged) .map(PullRequest::getMergedAt) - .anyMatch(date -> date.isAfter(OffsetDateTime.now().minusDays(30))); + .noneMatch(date -> date.isAfter(OffsetDateTime.now().minusDays(30))); } /** From 1219d970ace2956c8a25328744be242ddc83f540 Mon Sep 17 00:00:00 2001 From: GODrums Date: Tue, 3 Dec 2024 18:27:45 +0100 Subject: [PATCH 10/22] Extract algorithm constants --- .../LeaguePointsCalculationService.java | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java index bae5d9e8..abb9b1fc 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/leaderboard/LeaguePointsCalculationService.java @@ -11,10 +11,28 @@ public class LeaguePointsCalculationService { private final Logger logger = LoggerFactory.getLogger(LeaguePointsCalculationService.class); + // Starting points for new players + public static int POINTS_DEFAULT = 1000; + // Upper bound for first reducting in the k-factor + public static int POINTS_THRESHOLD_HIGH = 1750; + // Lower bound for first reducting in the k-factor + public static int POINTS_THRESHOLD_LOW = 1250; + // Minimum amount of points to decay each cycle + public static int DECAY_MINIMUM = 10; + // Factor to determine how much of the current points are decayed each cycle + public static double DECAY_FACTOR = 0.05; + // K-factors depending on the player's league points + public static double K_FACTOR_NEW_PLAYER = 2.0; + public static double K_FACTOR_LOW_POINTS = 1.5; + public static double K_FACTOR_MEDIUM_POINTS = 1.2; + public static double K_FACTOR_HIGH_POINTS = 1.1; + + + public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { - // Initialize new players with 1000 points + // Initialize points for new players if (user.getLeaguePoints() == 0) { - user.setLeaguePoints(1000); + user.setLeaguePoints(POINTS_DEFAULT); } int oldPoints = user.getLeaguePoints(); @@ -39,19 +57,21 @@ public int calculateNewPoints(User user, LeaderboardEntryDTO entry) { /** * Calculate the K factor for the user based on their current points and if they are a new player. + * The K-factor is used to control the sensitivity of the rating system to changes in the leaderboard. + * New players have a higher K-factor to allow them to quickly reach their true skill level. * @param user * @return K factor * @see Wikipedia: Most accurate K-factor */ private double getKFactor(User user) { if (isNewPlayer(user)) { - return 2.0; - } else if (user.getLeaguePoints() < 1400) { - return 1.5; - } else if (user.getLeaguePoints() < 1800) { - return 1.2; + return K_FACTOR_NEW_PLAYER; + } else if (user.getLeaguePoints() < POINTS_THRESHOLD_LOW) { + return K_FACTOR_LOW_POINTS; + } else if (user.getLeaguePoints() < POINTS_THRESHOLD_HIGH) { + return K_FACTOR_MEDIUM_POINTS; } else { - return 1.1; + return K_FACTOR_HIGH_POINTS; } } @@ -69,19 +89,29 @@ private boolean isNewPlayer(User user) { /** * Calculate the base decay in points based on the current points. - * @param currentPoints + * @param currentPoints Current amount of league points * @return Amount of decay points */ private int calculateDecay(int currentPoints) { - // 5% decay of current points, minimum 10 points if they have any points - return currentPoints > 0 ? Math.max(10, (int)(currentPoints * 0.05)) : 0; + // decay a part of the current points, at least DECAY_MINIMUM points + return currentPoints > 0 ? Math.max(DECAY_MINIMUM, (int)(currentPoints * DECAY_FACTOR)) : 0; } + /** + * Calculate the bonus points based on the leaderboard score. + * @param score Leaderboard score + * @return Bonus points + */ private int calculatePerformanceBonus(int score) { // Convert leaderboard score directly to points with diminishing returns return (int)(Math.sqrt(score) * 10); } + /** + * Calculate the bonus points based on the placement in the leaderboard. + * @param placement Placement in the leaderboard + * @return Bonus points + */ private int calculatePlacementBonus(int placement) { // Bonus for top 3 placements return placement <= 3 ? 20 * (4 - placement) : 0; From 28ede25f4295efabddd356623a1b8af17eeab5c3 Mon Sep 17 00:00:00 2001 From: GODrums Date: Wed, 4 Dec 2024 11:23:08 +0100 Subject: [PATCH 11/22] League Icons in Leaderboard --- server/application-server/openapi.yaml | 3 + .../gitprovider/user/UserInfoDTO.java | 6 +- .../core/modules/openapi/model/user-info.ts | 1 + .../leaderboard/leaderboard.component.html | 9 +- .../home/leaderboard/leaderboard.component.ts | 6 +- .../ui/league-icon/league-icon.component.ts | 54 +++++++++ .../src/app/user/header/header.component.html | 104 +++++++++--------- .../src/app/user/header/header.component.ts | 4 +- .../src/app/user/user-profile.component.html | 1 + webapp/src/app/utils.ts | 14 +++ webapp/src/styles.css | 6 + webapp/tailwind.config.ts | 17 +++ 12 files changed, 170 insertions(+), 55 deletions(-) create mode 100644 webapp/src/app/ui/league-icon/league-icon.component.ts diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index c014e924..31fc5bc3 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -622,6 +622,9 @@ components: type: string htmlUrl: type: string + leaguePoints: + type: integer + format: int32 TeamInfo: required: - color diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserInfoDTO.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserInfoDTO.java index 69792968..40e70c32 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserInfoDTO.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/UserInfoDTO.java @@ -10,7 +10,8 @@ public record UserInfoDTO( String email, @NonNull String avatarUrl, @NonNull String name, - @NonNull String htmlUrl) { + @NonNull String htmlUrl, + int leaguePoints) { public static UserInfoDTO fromUser(User user) { return new UserInfoDTO( @@ -19,6 +20,7 @@ public static UserInfoDTO fromUser(User user) { user.getEmail(), user.getAvatarUrl(), user.getName(), - user.getHtmlUrl()); + user.getHtmlUrl(), + user.getLeaguePoints()); } } diff --git a/webapp/src/app/core/modules/openapi/model/user-info.ts b/webapp/src/app/core/modules/openapi/model/user-info.ts index 1a5fb645..fb897ebc 100644 --- a/webapp/src/app/core/modules/openapi/model/user-info.ts +++ b/webapp/src/app/core/modules/openapi/model/user-info.ts @@ -18,5 +18,6 @@ export interface UserInfo { avatarUrl: string; name: string; htmlUrl: string; + leaguePoints?: number; } diff --git a/webapp/src/app/home/leaderboard/leaderboard.component.html b/webapp/src/app/home/leaderboard/leaderboard.component.html index 4b90467e..eb01e17d 100644 --- a/webapp/src/app/home/leaderboard/leaderboard.component.html +++ b/webapp/src/app/home/leaderboard/leaderboard.component.html @@ -2,8 +2,9 @@ Rank + League Contributor - + Score @@ -14,6 +15,9 @@ @if (isLoading()) { @for (entry of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track entry; let idx = $index) { + + + @@ -44,6 +48,9 @@ @for (entry of leaderboard(); track entry.user.login) { {{ entry.rank }} + + + diff --git a/webapp/src/app/home/leaderboard/leaderboard.component.ts b/webapp/src/app/home/leaderboard/leaderboard.component.ts index 583495a9..f584acee 100644 --- a/webapp/src/app/home/leaderboard/leaderboard.component.ts +++ b/webapp/src/app/home/leaderboard/leaderboard.component.ts @@ -16,6 +16,7 @@ import { TableComponent } from 'app/ui/table/table.component'; import { ReviewsPopoverComponent } from './reviews-popover/reviews-popover.component'; import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm'; import { lucideAward } from '@ng-icons/lucide'; +import { LeagueIconComponent } from '@app/ui/league-icon/league-icon.component'; @Component({ selector: 'app-leaderboard', @@ -32,8 +33,9 @@ import { lucideAward } from '@ng-icons/lucide'; ReviewsPopoverComponent, NgIconComponent, HlmIconComponent, - RouterLink - ], + RouterLink, + LeagueIconComponent +], providers: [provideIcons({ lucideAward })], templateUrl: './leaderboard.component.html' }) diff --git a/webapp/src/app/ui/league-icon/league-icon.component.ts b/webapp/src/app/ui/league-icon/league-icon.component.ts new file mode 100644 index 00000000..a0fa9ca7 --- /dev/null +++ b/webapp/src/app/ui/league-icon/league-icon.component.ts @@ -0,0 +1,54 @@ +import { Component, computed, input } from '@angular/core'; +import { cn, getLeagueFromPoints } from '@app/utils'; +import { LucideAngularModule, Crown } from 'lucide-angular'; +import { type VariantProps, cva } from 'class-variance-authority'; + +export const leagueVariants = cva( + 'size-8', + { + variants: { + size: { + default: '', + sm: 'size-6', + lg: 'size-12', + max: 'size-28', + full: 'size-full' + }, + league: { + none: 'text-gray-400', + bronze: 'text-league-bronze', + silver: 'text-league-silver', + gold: 'text-league-gold', + emerald: 'text-league-emerald', + diamond: 'text-league-diamond', + master: 'text-league-master' + } + }, + defaultVariants: { + size: 'default', + league: 'none' + } + } +); +type LeagueVariants = VariantProps; + +@Component({ + selector: 'app-icon-league', + standalone: true, + imports: [LucideAngularModule], + template: ` + + ` +}) +export class LeagueIconComponent { + protected Crown = Crown; + + size = input('default'); + leaguePoints = input(); + league = input('none'); + class = input(''); + + computedLeague = computed(() => this.leaguePoints() ? getLeagueFromPoints(this.leaguePoints()!) : this.league()); + + computedClass = computed(() => cn(leagueVariants({ size: this.size(), league: this.computedLeague() }), this.class())); +} diff --git a/webapp/src/app/user/header/header.component.html b/webapp/src/app/user/header/header.component.html index 3e6e9846..aeb63d75 100644 --- a/webapp/src/app/user/header/header.component.html +++ b/webapp/src/app/user/header/header.component.html @@ -1,52 +1,58 @@ -
- @if (isLoading()) { - - - - - } @else { - - - - {{ user()?.login?.slice(0, 2)?.toUpperCase() }} - - - } - @if (isLoading()) { -
- - - -
- - -
-
- } - @if (!isLoading() && user(); as user) { -
-

{{ user.name }}

- - github.com/{{ user.login }} - - @if (displayFirstContribution()) { -
- - Contributing since {{ displayFirstContribution() }} -
- } - @if (contributedRepositories(); as contributedRepositories) { +
+
+ @if (isLoading()) { + + + + + } @else { + + + + {{ user()?.login?.slice(0, 2)?.toUpperCase() }} + + + } + @if (isLoading()) { +
+ + +
- @for (repository of contributedRepositories; track repository) { - - - - - {{ repository.nameWithOwner }} - - } + +
- } -
- } +
+ } + @if (!isLoading() && user(); as user) { +
+

{{ user.name }}

+ + github.com/{{ user.login }} + + @if (displayFirstContribution()) { +
+ + Contributing since {{ displayFirstContribution() }} +
+ } + @if (contributedRepositories(); as contributedRepositories) { +
+ @for (repository of contributedRepositories; track repository) { + + + + + {{ repository.nameWithOwner }} + + } +
+ } +
+ } +
+
+ + {{ leaguePoints() ?? 0 }} +
diff --git a/webapp/src/app/user/header/header.component.ts b/webapp/src/app/user/header/header.component.ts index f232a8a5..c0718423 100644 --- a/webapp/src/app/user/header/header.component.ts +++ b/webapp/src/app/user/header/header.component.ts @@ -10,6 +10,7 @@ import { HlmButtonModule } from '@spartan-ng/ui-button-helm'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; import { RepositoryInfo, UserInfo } from '@app/core/modules/openapi'; +import { LeagueIconComponent } from '@app/ui/league-icon/league-icon.component'; dayjs.extend(advancedFormat); @@ -22,7 +23,7 @@ const repoImages: { [key: string]: string } = { @Component({ selector: 'app-user-header', standalone: true, - imports: [NgIconComponent, HlmAvatarModule, HlmSkeletonModule, HlmIconModule, HlmTooltipComponent, HlmTooltipTriggerDirective, BrnTooltipContentDirective, HlmButtonModule], + imports: [NgIconComponent, HlmAvatarModule, HlmSkeletonModule, HlmIconModule, HlmTooltipComponent, HlmTooltipTriggerDirective, BrnTooltipContentDirective, HlmButtonModule, LeagueIconComponent], templateUrl: './header.component.html' }) export class UserHeaderComponent { @@ -32,6 +33,7 @@ export class UserHeaderComponent { user = input(); firstContribution = input(); contributedRepositories = input(); + leaguePoints = input(); displayFirstContribution = computed(() => { if (this.firstContribution()) { diff --git a/webapp/src/app/user/user-profile.component.html b/webapp/src/app/user/user-profile.component.html index 74e7c1ee..8d4320b5 100644 --- a/webapp/src/app/user/user-profile.component.html +++ b/webapp/src/app/user/user-profile.component.html @@ -13,6 +13,7 @@

Something went wrong...

[user]="query.data()?.userInfo" [firstContribution]="query.data()?.firstContribution" [contributedRepositories]="query.data()?.contributedRepositories" + [leaguePoints]="query.data()?.userInfo?.leaguePoints" [isLoading]="showSkeleton" />
diff --git a/webapp/src/app/utils.ts b/webapp/src/app/utils.ts index 20216257..5affa6e9 100644 --- a/webapp/src/app/utils.ts +++ b/webapp/src/app/utils.ts @@ -15,3 +15,17 @@ export function groupBy(arr: T[], key: (i: T) => K) { {} as Record ); } + +export function getLeagueFromPoints(points: number) { + if (points < 1250) { + return 'bronze'; + } else if (points < 1500) { + return 'silver'; + } else if (points < 1750) { + return 'gold'; + } else if (points < 2000) { + return 'diamond'; + } else { + return 'master'; + } +} diff --git a/webapp/src/styles.css b/webapp/src/styles.css index e922cf09..fc048183 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -32,6 +32,12 @@ --input: 240 5.9% 90%; --ring: 240 5.9% 10%; --radius: 0.5rem; + + --league-bronze: 30 58% 54%; + --league-silver: 0 0% 77%; + --league-gold: 48 97% 48%; + --league-diamond: 225 73% 53%; + --league-master: 330 100% 71; } .dark { diff --git a/webapp/tailwind.config.ts b/webapp/tailwind.config.ts index 9a8b8a5b..90bbeef9 100644 --- a/webapp/tailwind.config.ts +++ b/webapp/tailwind.config.ts @@ -126,6 +126,23 @@ const config = { foreground: "var(--fgColor-sponsors)", }, }, + league: { + bronze: { + DEFAULT: "hsl(var(--league-bronze))", + }, + silver: { + DEFAULT: "hsl(var(--league-silver))", + }, + gold: { + DEFAULT: "hsl(var(--league-gold))", + }, + diamond: { + DEFAULT: "hsl(var(--league-diamond))", + }, + master: { + DEFAULT: "hsl(var(--league-master))", + }, + } }, borderRadius: { lg: "var(--radius)", From 8c66e4d5c1fe78becd8d8e957607f88562dcf4c9 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sun, 8 Dec 2024 23:21:57 +0100 Subject: [PATCH 12/22] Add League Overview --- webapp/src/app/home/home.component.html | 3 +- webapp/src/app/home/home.component.ts | 3 +- .../home/leaderboard/leaderboard.component.ts | 2 +- .../leaderboard/league/league.component.html | 16 ++++++++ .../leaderboard/league/league.component.ts | 20 ++++++++++ .../home/leaderboard/league/league.stories.ts | 26 ++++++++++++ .../league/elo-card/elo-card.component.html | 26 ++++++++++++ .../ui/league/elo-card/elo-card.component.ts | 38 ++++++++++++++++++ .../ui/league/elo-card/elo-card.stories.ts | 26 ++++++++++++ .../icon}/league-icon.component.ts | 4 +- .../info-modal/info-modal.component.html | 26 ++++++++++++ .../league/info-modal/info-modal.component.ts | 36 +++++++++++++++++ .../league/info-modal/info-modal.stories.ts | 15 +++++++ .../src/app/user/header/header.component.ts | 2 +- webapp/src/app/utils.ts | 40 ++++++++++++++----- 15 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 webapp/src/app/home/leaderboard/league/league.component.html create mode 100644 webapp/src/app/home/leaderboard/league/league.component.ts create mode 100644 webapp/src/app/home/leaderboard/league/league.stories.ts create mode 100644 webapp/src/app/ui/league/elo-card/elo-card.component.html create mode 100644 webapp/src/app/ui/league/elo-card/elo-card.component.ts create mode 100644 webapp/src/app/ui/league/elo-card/elo-card.stories.ts rename webapp/src/app/ui/{league-icon => league/icon}/league-icon.component.ts (90%) create mode 100644 webapp/src/app/ui/league/info-modal/info-modal.component.html create mode 100644 webapp/src/app/ui/league/info-modal/info-modal.component.ts create mode 100644 webapp/src/app/ui/league/info-modal/info-modal.stories.ts diff --git a/webapp/src/app/home/home.component.html b/webapp/src/app/home/home.component.html index 0bf9252a..84d2953f 100644 --- a/webapp/src/app/home/home.component.html +++ b/webapp/src/app/home/home.component.html @@ -10,7 +10,7 @@

Hi {{ userValue.name }} 👋

-
+
@if (query.error()) {
@@ -18,6 +18,7 @@

Something went wrong...

We couldn't load the leaderboard. Please try again later.

} @else { +
diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts index 8a884ab1..b72881b6 100644 --- a/webapp/src/app/home/home.component.ts +++ b/webapp/src/app/home/home.component.ts @@ -13,13 +13,14 @@ import { SecurityStore } from '@app/core/security/security-store.service'; import { HlmAlertModule } from '@spartan-ng/ui-alert-helm'; import { MetaService } from '@app/core/modules/openapi'; import { LeaderboardLegendComponent } from './leaderboard/legend/legends.component'; +import { LeaderboardLeagueComponent } from './leaderboard/league/league.component'; dayjs.extend(isoWeek); @Component({ selector: 'app-home', standalone: true, - imports: [LeaderboardComponent, LeaderboardFilterComponent, HlmAlertModule, LucideAngularModule, LeaderboardLegendComponent], + imports: [LeaderboardComponent, LeaderboardFilterComponent, HlmAlertModule, LucideAngularModule, LeaderboardLegendComponent, LeaderboardLeagueComponent], templateUrl: './home.component.html' }) export class HomeComponent { diff --git a/webapp/src/app/home/leaderboard/leaderboard.component.ts b/webapp/src/app/home/leaderboard/leaderboard.component.ts index f584acee..6ddb46fc 100644 --- a/webapp/src/app/home/leaderboard/leaderboard.component.ts +++ b/webapp/src/app/home/leaderboard/leaderboard.component.ts @@ -16,7 +16,7 @@ import { TableComponent } from 'app/ui/table/table.component'; import { ReviewsPopoverComponent } from './reviews-popover/reviews-popover.component'; import { HlmIconComponent, provideIcons } from '@spartan-ng/ui-icon-helm'; import { lucideAward } from '@ng-icons/lucide'; -import { LeagueIconComponent } from '@app/ui/league-icon/league-icon.component'; +import { LeagueIconComponent } from '@app/ui/league/icon/league-icon.component'; @Component({ selector: 'app-leaderboard', diff --git a/webapp/src/app/home/leaderboard/league/league.component.html b/webapp/src/app/home/leaderboard/league/league.component.html new file mode 100644 index 00000000..59e76ce6 --- /dev/null +++ b/webapp/src/app/home/leaderboard/league/league.component.html @@ -0,0 +1,16 @@ +
+
+ +

Your League

+ +
+
+ +
+ + +
+
+
diff --git a/webapp/src/app/home/leaderboard/league/league.component.ts b/webapp/src/app/home/leaderboard/league/league.component.ts new file mode 100644 index 00000000..1fa78221 --- /dev/null +++ b/webapp/src/app/home/leaderboard/league/league.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; +import { HlmCardModule } from '@spartan-ng/ui-card-helm'; +import { HlmButtonModule } from '@spartan-ng/ui-button-helm'; +import { LeagueEloCardComponent } from '@app/ui/league/elo-card/elo-card.component'; +import { LeagueInfoModalComponent } from '@app/ui/league/info-modal/info-modal.component'; + +@Component({ + selector: 'app-leaderboard-league', + standalone: true, + imports: [ + HlmCardModule, + HlmButtonModule, + LeagueEloCardComponent, + LeagueInfoModalComponent + ], + templateUrl: './league.component.html' +}) +export class LeaderboardLeagueComponent { + leaguePoints = input(); +} diff --git a/webapp/src/app/home/leaderboard/league/league.stories.ts b/webapp/src/app/home/leaderboard/league/league.stories.ts new file mode 100644 index 00000000..208dd3ae --- /dev/null +++ b/webapp/src/app/home/leaderboard/league/league.stories.ts @@ -0,0 +1,26 @@ +import { type Meta, type StoryObj } from '@storybook/angular'; +import { LeaderboardLeagueComponent } from './league.component'; + +const meta: Meta = { + component: LeaderboardLeagueComponent, + tags: ['autodocs'], + parameters: { + layout: 'centered' + }, + args: { + leaguePoints: 1100 + }, + argTypes: { + leaguePoints: { + control: { + type: 'number' + }, + description: 'Current League Points to be displayed' + } + } +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/webapp/src/app/ui/league/elo-card/elo-card.component.html b/webapp/src/app/ui/league/elo-card/elo-card.component.html new file mode 100644 index 00000000..2b884a0d --- /dev/null +++ b/webapp/src/app/ui/league/elo-card/elo-card.component.html @@ -0,0 +1,26 @@ +
+ + @if (currentLeague(); as currentLeague) { +
+ {{ currentLeague.name }} + +
+
+ @if (currentLeague.maxPoints === Infinity) { + {{ leaguePoints() }} + } @else { + {{ leaguePoints() }} / {{ currentLeague.maxPoints }} + } + +
+
+ +
+ + + + +
+
+ } +
diff --git a/webapp/src/app/ui/league/elo-card/elo-card.component.ts b/webapp/src/app/ui/league/elo-card/elo-card.component.ts new file mode 100644 index 00000000..bc4d0e8c --- /dev/null +++ b/webapp/src/app/ui/league/elo-card/elo-card.component.ts @@ -0,0 +1,38 @@ +import { Component, computed, input, signal } from '@angular/core'; +import { HlmCardModule } from '@spartan-ng/ui-card-helm'; +import { BrnProgressComponent, BrnProgressIndicatorComponent } from '@spartan-ng/ui-progress-brain'; +import { HlmProgressIndicatorDirective } from '@spartan-ng/ui-progress-helm'; +import { LeagueIconComponent } from '@app/ui/league/icon/league-icon.component'; +import { getLeagueFromPoints } from '@app/utils'; +import { HlmButtonModule } from '@spartan-ng/ui-button-helm'; +import { LucideAngularModule, Star } from 'lucide-angular'; + +@Component({ + selector: 'app-league-elo-card', + standalone: true, + imports: [ + HlmCardModule, + LeagueIconComponent, + BrnProgressComponent, + BrnProgressIndicatorComponent, + HlmProgressIndicatorDirective, + HlmButtonModule, + LucideAngularModule + ], + templateUrl: './elo-card.component.html' +}) +export class LeagueEloCardComponent { + protected Star = Star; + protected Infinity = Infinity; + leaguePoints = input(); + + currentLeague = computed(() => getLeagueFromPoints(this.leaguePoints()!)); + + progressValue = signal(0); + + ngOnInit() { + if (this.leaguePoints() && this.currentLeague()) { + this.progressValue.set((this.leaguePoints()! - this.currentLeague()!.minPoints) * 100 / (this.currentLeague()!.maxPoints - this.currentLeague()!.minPoints)); + } + } +} diff --git a/webapp/src/app/ui/league/elo-card/elo-card.stories.ts b/webapp/src/app/ui/league/elo-card/elo-card.stories.ts new file mode 100644 index 00000000..2e6c54e1 --- /dev/null +++ b/webapp/src/app/ui/league/elo-card/elo-card.stories.ts @@ -0,0 +1,26 @@ +import { type Meta, type StoryObj } from '@storybook/angular'; +import { LeagueEloCardComponent } from './elo-card.component'; + +const meta: Meta = { + component: LeagueEloCardComponent, + tags: ['autodocs'], + parameters: { + layout: 'centered' + }, + args: { + leaguePoints: 1100 + }, + argTypes: { + leaguePoints: { + control: { + type: 'number' + }, + description: 'Current League Points to be displayed' + } + } +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/webapp/src/app/ui/league-icon/league-icon.component.ts b/webapp/src/app/ui/league/icon/league-icon.component.ts similarity index 90% rename from webapp/src/app/ui/league-icon/league-icon.component.ts rename to webapp/src/app/ui/league/icon/league-icon.component.ts index a0fa9ca7..b8403268 100644 --- a/webapp/src/app/ui/league-icon/league-icon.component.ts +++ b/webapp/src/app/ui/league/icon/league-icon.component.ts @@ -48,7 +48,7 @@ export class LeagueIconComponent { league = input('none'); class = input(''); - computedLeague = computed(() => this.leaguePoints() ? getLeagueFromPoints(this.leaguePoints()!) : this.league()); + computedLeague = computed(() => this.leaguePoints() ? getLeagueFromPoints(this.leaguePoints()!)?.name.toLowerCase() : this.league()); - computedClass = computed(() => cn(leagueVariants({ size: this.size(), league: this.computedLeague() }), this.class())); + computedClass = computed(() => cn(leagueVariants({ size: this.size(), league: this.computedLeague() as LeagueVariants['league'] }), this.class())); } diff --git a/webapp/src/app/ui/league/info-modal/info-modal.component.html b/webapp/src/app/ui/league/info-modal/info-modal.component.html new file mode 100644 index 00000000..5809c045 --- /dev/null +++ b/webapp/src/app/ui/league/info-modal/info-modal.component.html @@ -0,0 +1,26 @@ + + + + +

All Leagues

+
+
+ @for (league of Leagues; track league.name) { +
+ + {{ league.name }} +
+ + @if (league.maxPoints === Infinity) { + {{ league.maxPoints }}+ + } @else { + {{ league.minPoints }} - {{ league.maxPoints }} + } +
+
+ } +
+
+
diff --git a/webapp/src/app/ui/league/info-modal/info-modal.component.ts b/webapp/src/app/ui/league/info-modal/info-modal.component.ts new file mode 100644 index 00000000..90ddc8a4 --- /dev/null +++ b/webapp/src/app/ui/league/info-modal/info-modal.component.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { HlmCardModule } from '@spartan-ng/ui-card-helm'; +import { HlmButtonModule } from '@spartan-ng/ui-button-helm'; +import { BrnDialogContentDirective, BrnDialogTriggerDirective } from '@spartan-ng/ui-dialog-brain'; +import { + HlmDialogComponent, + HlmDialogContentComponent, + HlmDialogHeaderComponent, +} from '@spartan-ng/ui-dialog-helm'; +import { LucideAngularModule, Info, Star } from 'lucide-angular'; +import { Leagues } from '@app/utils'; +import { LeagueIconComponent } from '@app/ui/league/icon/league-icon.component'; + +@Component({ + selector: 'app-league-info-modal', + standalone: true, + imports: [ + HlmCardModule, + HlmButtonModule, + HlmDialogComponent, + HlmDialogContentComponent, + HlmDialogHeaderComponent, + BrnDialogContentDirective, + BrnDialogTriggerDirective, + LucideAngularModule, + LeagueIconComponent + ], + templateUrl: './info-modal.component.html' +}) +export class LeagueInfoModalComponent { + protected Info = Info; + protected Star = Star; + + protected Infinity = Infinity; + protected Leagues = Leagues; +} diff --git a/webapp/src/app/ui/league/info-modal/info-modal.stories.ts b/webapp/src/app/ui/league/info-modal/info-modal.stories.ts new file mode 100644 index 00000000..2421b05d --- /dev/null +++ b/webapp/src/app/ui/league/info-modal/info-modal.stories.ts @@ -0,0 +1,15 @@ +import { type Meta, type StoryObj } from '@storybook/angular'; +import { LeagueInfoModalComponent } from './info-modal.component'; + +const meta: Meta = { + component: LeagueInfoModalComponent, + tags: ['autodocs'], + parameters: { + layout: 'centered' + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/webapp/src/app/user/header/header.component.ts b/webapp/src/app/user/header/header.component.ts index c0718423..9dc1e78f 100644 --- a/webapp/src/app/user/header/header.component.ts +++ b/webapp/src/app/user/header/header.component.ts @@ -10,7 +10,7 @@ import { HlmButtonModule } from '@spartan-ng/ui-button-helm'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; import { RepositoryInfo, UserInfo } from '@app/core/modules/openapi'; -import { LeagueIconComponent } from '@app/ui/league-icon/league-icon.component'; +import { LeagueIconComponent } from '@app/ui/league/icon/league-icon.component'; dayjs.extend(advancedFormat); diff --git a/webapp/src/app/utils.ts b/webapp/src/app/utils.ts index 5affa6e9..24ee6a2a 100644 --- a/webapp/src/app/utils.ts +++ b/webapp/src/app/utils.ts @@ -17,15 +17,33 @@ export function groupBy(arr: T[], key: (i: T) => K) { } export function getLeagueFromPoints(points: number) { - if (points < 1250) { - return 'bronze'; - } else if (points < 1500) { - return 'silver'; - } else if (points < 1750) { - return 'gold'; - } else if (points < 2000) { - return 'diamond'; - } else { - return 'master'; - } + return Leagues.find(league => points >= league.minPoints && points < league.maxPoints); } + +export const Leagues = [ + { + name: 'Bronze', + minPoints: 0, + maxPoints: 1250 + }, + { + name: 'Silver', + minPoints: 1250, + maxPoints: 1500 + }, + { + name: 'Gold', + minPoints: 1500, + maxPoints: 1750 + }, + { + name: 'Diamond', + minPoints: 1750, + maxPoints: 2000 + }, + { + name: 'Master', + minPoints: 2000, + maxPoints: Infinity + } +] From 2c1d89db44e29a1efe7915026d090ff8a21e7804 Mon Sep 17 00:00:00 2001 From: GODrums Date: Mon, 30 Dec 2024 16:11:22 +0100 Subject: [PATCH 13/22] chore: run prettier --- .../tum/in/www1/hephaestus/Application.java | 8 +- ...assImportIntegratorIntegratorProvider.java | 46 ++- .../www1/hephaestus/OpenAPIConfiguration.java | 111 ++++---- .../in/www1/hephaestus/SecurityConfig.java | 33 ++- .../de/tum/in/www1/hephaestus/WebConfig.java | 3 +- .../www1/hephaestus/config/GitHubConfig.java | 5 +- .../config/IntelligenceServiceConfig.java | 6 +- .../hephaestus/config/KeycloakConfig.java | 4 +- .../in/www1/hephaestus/config/NatsConfig.java | 9 +- .../config/SentryConfiguration.java | 2 +- .../hephaestus/config/SlackAppConfig.java | 8 +- .../hephaestus/config/SpringAsyncConfig.java | 2 +- .../exception/AccessForbiddenException.java | 1 - .../exception/EntityNotFoundException.java | 1 - .../gitprovider/common/AuthorAssociation.java | 2 +- .../common/BaseGitServiceEntity.java | 4 +- .../common/BaseGitServiceEntityConverter.java | 11 +- .../gitprovider/common/DateUtil.java | 1 + .../GitHubAuthorAssociationConverter.java | 3 +- .../common/github/GitHubMessageHandler.java | 6 +- .../github/GitHubMessageHandlerRegistry.java | 3 +- .../hephaestus/gitprovider/issue/Issue.java | 54 ++-- .../gitprovider/issue/IssueInfoDTO.java | 11 +- .../issue/github/GitHubIssueConverter.java | 7 +- .../github/GitHubIssueMessageHandler.java | 26 +- .../issue/github/GitHubIssueSyncService.java | 139 +++++---- .../issuecomment/IssueComment.java | 19 +- .../issuecomment/IssueCommentInfoDTO.java | 12 +- .../github/GitHubIssueCommentConverter.java | 8 +- .../GitHubIssueCommentMessageHandler.java | 30 +- .../github/GitHubIssueCommentSyncService.java | 76 ++--- .../hephaestus/gitprovider/label/Label.java | 17 +- .../gitprovider/label/LabelInfoDTO.java | 21 +- .../gitprovider/label/LabelRepository.java | 6 +- .../label/github/GitHubLabelConverter.java | 3 +- .../github/GitHubLabelMessageHandler.java | 22 +- .../label/github/GitHubLabelSyncService.java | 29 +- .../gitprovider/milestone/Milestone.java | 20 +- .../milestone/MilestoneInfoDTO.java | 7 +- .../milestone/MilestoneRepository.java | 3 +- .../github/GitHubMilestoneConverter.java | 9 +- .../github/GitHubMilestoneMessageHandler.java | 24 +- .../github/GitHubMilestoneSyncService.java | 39 +-- .../gitprovider/pullrequest/PullRequest.java | 31 +- .../pullrequest/PullRequestBaseInfoDTO.java | 57 ++-- .../pullrequest/PullRequestInfoDTO.java | 104 +++---- .../pullrequest/PullRequestRepository.java | 74 ++--- .../github/GitHubPullRequestConverter.java | 16 +- .../GitHubPullRequestMessageHandler.java | 20 +- .../github/GitHubPullRequestSyncService.java | 193 +++++++------ .../pullrequestreview/PullRequestReview.java | 29 +- .../PullRequestReviewInfoDTO.java | 24 +- .../GitHubPullRequestReviewConverter.java | 8 +- ...GitHubPullRequestReviewMessageHandler.java | 23 +- .../GitHubPullRequestReviewSyncService.java | 56 ++-- .../PullRequestReviewComment.java | 21 +- .../PullRequestReviewCommentRepository.java | 4 +- ...tHubPullRequestReviewCommentConverter.java | 15 +- ...ullRequestReviewCommentMessageHandler.java | 30 +- ...ubPullRequestReviewCommentSyncService.java | 113 +++++--- .../gitprovider/repository/Repository.java | 31 +- .../repository/RepositoryInfoDTO.java | 31 +- .../repository/RepositoryRepository.java | 20 +- .../github/GitHubRepositoryConverter.java | 9 +- .../github/GitHubRepositorySyncService.java | 52 ++-- .../hephaestus/gitprovider/team/Team.java | 29 +- .../gitprovider/team/TeamController.java | 2 +- .../gitprovider/team/TeamRepository.java | 45 +-- .../gitprovider/team/TeamService.java | 143 +++++++--- .../hephaestus/gitprovider/user/User.java | 34 ++- .../gitprovider/user/UserController.java | 5 +- .../gitprovider/user/UserInfoDTO.java | 33 +-- .../gitprovider/user/UserProfileDTO.java | 20 +- .../gitprovider/user/UserRepository.java | 110 +++---- .../gitprovider/user/UserService.java | 66 ++--- .../gitprovider/user/UserTeamsDTO.java | 23 +- .../user/github/GitHubUserConverter.java | 7 +- .../user/github/GitHubUserSyncService.java | 48 ++-- .../intelligenceservice/ApiClient.java | 187 +++++++----- .../intelligenceservice/BaseApi.java | 17 +- .../BeanValidationException.java | 5 +- .../JavaTimeFormatter.java | 6 +- .../RFC3339DateFormat.java | 69 ++--- .../ServerConfiguration.java | 12 +- .../intelligenceservice/ServerVariable.java | 6 +- .../api/HealthcheckApi.java | 74 +++-- .../intelligenceservice/api/MentorApi.java | 98 ++++--- .../intelligenceservice/auth/ApiKeyAuth.java | 12 +- .../auth/Authentication.java | 6 +- .../auth/HttpBasicAuth.java | 18 +- .../auth/HttpBearerAuth.java | 17 +- .../model/ISHTTPValidationError.java | 143 +++++----- .../model/ISHealthCheck.java | 131 ++++----- .../model/ISMentorMessage.java | 131 ++++----- .../intelligenceservice/model/ISMessage.java | 202 +++++++------ .../model/ISMessageHistory.java | 143 +++++----- .../model/ISValidationError.java | 269 +++++++++--------- .../model/ISValidationErrorLocInner.java | 79 +++-- .../leaderboard/LeaderboardController.java | 8 +- .../leaderboard/LeaderboardEntryDTO.java | 22 +- .../leaderboard/LeaderboardService.java | 48 ++-- .../leaderboard/LeaderboardTaskScheduler.java | 4 +- .../LeaguePointsCalculationService.java | 31 +- .../leaderboard/ScoringService.java | 29 +- .../tasks/LeaguePointsUpdateTask.java | 3 +- .../tasks/SlackWeeklyLeaderboardTask.java | 1 + .../hephaestus/mentor/message/Message.java | 6 +- .../mentor/message/MessageController.java | 9 +- .../hephaestus/mentor/message/MessageDTO.java | 36 +-- .../mentor/message/MessageRepository.java | 5 +- .../mentor/message/MessageService.java | 37 +-- .../hephaestus/mentor/session/Session.java | 9 +- .../mentor/session/SessionController.java | 14 +- .../hephaestus/mentor/session/SessionDTO.java | 13 +- .../mentor/session/SessionRepository.java | 5 +- .../mentor/session/SessionService.java | 10 +- .../www1/hephaestus/meta/MetaController.java | 2 +- .../syncing/GitHubDataSyncScheduler.java | 5 +- .../syncing/GitHubDataSyncService.java | 117 ++++---- .../syncing/NatsConsumerService.java | 95 ++++--- .../workspace/NoWorkspaceFoundException.java | 1 + .../workspace/RepositoryToMonitor.java | 2 +- .../RepositoryToMonitorRepository.java | 5 +- .../www1/hephaestus/workspace/Workspace.java | 13 +- .../workspace/WorkspaceController.java | 80 +++--- .../workspace/WorkspaceService.java | 50 ++-- .../HephaestusApplicationTests.java | 13 +- 127 files changed, 2533 insertions(+), 2142 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java index 2174fba4..f28afbab 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/Application.java @@ -7,8 +7,8 @@ @SpringBootApplication public class Application { - public static void main(String[] args) { - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin")); - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin")); + SpringApplication.run(Application.class, args); + } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java index 7e7b9a26..7ef86bf6 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/ClassImportIntegratorIntegratorProvider.java @@ -1,11 +1,5 @@ package de.tum.in.www1.hephaestus; -import java.util.ArrayList; -import java.util.List; - -import org.hibernate.integrator.spi.Integrator; -import org.hibernate.jpa.boot.spi.IntegratorProvider; - import de.tum.in.www1.hephaestus.gitprovider.issue.IssueInfoDTO; import de.tum.in.www1.hephaestus.gitprovider.issuecomment.IssueCommentInfoDTO; import de.tum.in.www1.hephaestus.gitprovider.label.LabelInfoDTO; @@ -18,26 +12,30 @@ import de.tum.in.www1.hephaestus.mentor.message.MessageDTO; import de.tum.in.www1.hephaestus.mentor.session.SessionDTO; import io.hypersistence.utils.hibernate.type.util.ClassImportIntegrator; +import java.util.ArrayList; +import java.util.List; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; public class ClassImportIntegratorIntegratorProvider implements IntegratorProvider { - @Override - public List getIntegrators() { - // Accessible DTOs - @SuppressWarnings("rawtypes") - List classes = new ArrayList<>(); - classes.add(UserInfoDTO.class); - classes.add(TeamInfoDTO.class); - classes.add(IssueInfoDTO.class); - classes.add(LabelInfoDTO.class); - classes.add(MilestoneInfoDTO.class); - classes.add(PullRequestInfoDTO.class); - classes.add(IssueCommentInfoDTO.class); - classes.add(PullRequestReviewInfoDTO.class); - classes.add(RepositoryInfoDTO.class); - classes.add(MessageDTO.class); - classes.add(SessionDTO.class); + @Override + public List getIntegrators() { + // Accessible DTOs + @SuppressWarnings("rawtypes") + List classes = new ArrayList<>(); + classes.add(UserInfoDTO.class); + classes.add(TeamInfoDTO.class); + classes.add(IssueInfoDTO.class); + classes.add(LabelInfoDTO.class); + classes.add(MilestoneInfoDTO.class); + classes.add(PullRequestInfoDTO.class); + classes.add(IssueCommentInfoDTO.class); + classes.add(PullRequestReviewInfoDTO.class); + classes.add(RepositoryInfoDTO.class); + classes.add(MessageDTO.class); + classes.add(SessionDTO.class); - return List.of(new ClassImportIntegrator(classes)); - } + return List.of(new ClassImportIntegrator(classes)); + } } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/OpenAPIConfiguration.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/OpenAPIConfiguration.java index 19f34ab2..b3ba3829 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/OpenAPIConfiguration.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/OpenAPIConfiguration.java @@ -37,18 +37,20 @@ public OpenApiCustomizer schemaCustomizer() { if (components != null && components.getSchemas() != null) { // Only include schemas with DTO suffix and remove the suffix var schemas = components - .getSchemas() - .entrySet() - .stream() - .filter(entry -> entry.getKey().endsWith("DTO")) - .collect(Collectors.toMap( + .getSchemas() + .entrySet() + .stream() + .filter(entry -> entry.getKey().endsWith("DTO")) + .collect( + Collectors.toMap( entry -> entry.getKey().substring(0, entry.getKey().length() - 3), entry -> { var schema = entry.getValue(); schema.setName(entry.getKey().substring(0, entry.getKey().length() - 3)); return schema; } - )); + ) + ); // Remove DTO suffix from attribute names schemas.forEach((key, value) -> { @@ -70,50 +72,59 @@ public OpenApiCustomizer schemaCustomizer() { if (paths != null) { paths.forEach((path, pathItem) -> { logger.info("Processing path: {}", path); - pathItem.readOperations().forEach(operation -> { - // Remove DTO suffix from response schemas - var responses = operation.getResponses(); - if (responses != null) { - responses.forEach((responseCode, response) -> { - var content = response.getContent(); - if (content != null) { - content.forEach((contentType, mediaType) -> { - if (mediaType != null && mediaType.getSchema() != null) { - removeDTOSuffixesFromSchemaRecursively(mediaType.getSchema()); - } else { - logger.warn("MediaType or Schema is null for content type: {}", contentType); - } - }); - } else { - logger.warn("Response with code {} has no content.", responseCode); - } - }); - } - if (operation.getRequestBody() != null) { - var requestBodyContent = operation.getRequestBody().getContent(); - requestBodyContent.forEach((contentType, mediaType) -> { - removeDTOSuffixesFromSchemaRecursively(mediaType.getSchema()); - }); - } + pathItem + .readOperations() + .forEach(operation -> { + // Remove DTO suffix from response schemas + var responses = operation.getResponses(); + if (responses != null) { + responses.forEach((responseCode, response) -> { + var content = response.getContent(); + if (content != null) { + content.forEach((contentType, mediaType) -> { + if (mediaType != null && mediaType.getSchema() != null) { + removeDTOSuffixesFromSchemaRecursively(mediaType.getSchema()); + } else { + logger.warn( + "MediaType or Schema is null for content type: {}", + contentType + ); + } + }); + } else { + logger.warn("Response with code {} has no content.", responseCode); + } + }); + } + if (operation.getRequestBody() != null) { + var requestBodyContent = operation.getRequestBody().getContent(); + requestBodyContent.forEach((contentType, mediaType) -> { + removeDTOSuffixesFromSchemaRecursively(mediaType.getSchema()); + }); + } - // Remove -controller suffix from tags - if (operation.getTags() != null) { - operation.setTags( - operation.getTags() - .stream() - .filter(tag -> { - if (tag.length() > 11) { - return true; - } else { - logger.warn("Tag '{}' is shorter than expected and cannot be trimmed.", tag); - return false; - } - }) - .map(tag -> tag.substring(0, tag.length() - 11)) - .collect(Collectors.toList()) - ); - } - }); + // Remove -controller suffix from tags + if (operation.getTags() != null) { + operation.setTags( + operation + .getTags() + .stream() + .filter(tag -> { + if (tag.length() > 11) { + return true; + } else { + logger.warn( + "Tag '{}' is shorter than expected and cannot be trimmed.", + tag + ); + return false; + } + }) + .map(tag -> tag.substring(0, tag.length() - 11)) + .collect(Collectors.toList()) + ); + } + }); }); } else { logger.warn("Paths are null in OpenAPI configuration."); @@ -136,4 +147,4 @@ private void removeDTOSuffixesFromSchemaRecursively(Schema schema) { removeDTOSuffixesFromSchemaRecursively(schema.getItems()); } } -} \ No newline at end of file +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java index f8dec0f9..a69214af 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/SecurityConfig.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Stream; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; @@ -25,8 +24,8 @@ @Configuration @EnableWebSecurity public class SecurityConfig { - interface AuthoritiesConverter extends Converter, Collection> { - } + + interface AuthoritiesConverter extends Converter, Collection> {} @SuppressWarnings("unchecked") @Bean @@ -34,14 +33,19 @@ AuthoritiesConverter realmRolesAuthoritiesConverter() { return claims -> { final var realmAccess = Optional.ofNullable((Map) claims.get("realm_access")); final var roles = realmAccess.flatMap(map -> Optional.ofNullable((List) map.get("roles"))); - return roles.map(List::stream).orElse(Stream.empty()).map(SimpleGrantedAuthority::new) - .map(GrantedAuthority.class::cast).toList(); + return roles + .map(List::stream) + .orElse(Stream.empty()) + .map(SimpleGrantedAuthority::new) + .map(GrantedAuthority.class::cast) + .toList(); }; } @Bean JwtAuthenticationConverter authenticationConverter( - Converter, Collection> authoritiesConverter) { + Converter, Collection> authoritiesConverter + ) { var authenticationConverter = new JwtAuthenticationConverter(); authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> { return authoritiesConverter.convert(jwt.getClaims()); @@ -51,18 +55,21 @@ JwtAuthenticationConverter authenticationConverter( @Bean SecurityFilterChain resourceServerSecurityFilterChain( - HttpSecurity http, - Converter authenticationConverter) throws Exception { + HttpSecurity http, + Converter authenticationConverter + ) throws Exception { http.oauth2ResourceServer(resourceServer -> { resourceServer.jwt(jwtDecoder -> { jwtDecoder.jwtAuthenticationConverter(authenticationConverter); }); }); - http.sessionManagement(sessions -> { - sessions.sessionCreationPolicy(SessionCreationPolicy.STATELESS); - }).csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource())); + http + .sessionManagement(sessions -> { + sessions.sessionCreationPolicy(SessionCreationPolicy.STATELESS); + }) + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())); http.authorizeHttpRequests(requests -> { requests.requestMatchers("/workspace/**").hasAuthority("admin"); @@ -82,4 +89,4 @@ CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", configuration); return source; } -} \ No newline at end of file +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java index ba4fc629..35d8d0bf 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java @@ -5,7 +5,6 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - @Configuration public class WebConfig implements WebMvcConfigurer { @@ -13,4 +12,4 @@ public class WebConfig implements WebMvcConfigurer { public RestTemplate restTemplate() { return new RestTemplate(); } -} \ No newline at end of file +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java index b2c8cd83..51b8dbdf 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/GitHubConfig.java @@ -1,7 +1,8 @@ package de.tum.in.www1.hephaestus.config; import java.io.IOException; - +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -9,8 +10,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.kohsuke.github.GitHub; -import org.kohsuke.github.GitHubBuilder; @Configuration public class GitHubConfig { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/IntelligenceServiceConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/IntelligenceServiceConfig.java index de8f6608..327f4e3c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/IntelligenceServiceConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/IntelligenceServiceConfig.java @@ -1,12 +1,11 @@ package de.tum.in.www1.hephaestus.config; +import de.tum.in.www1.hephaestus.intelligenceservice.ApiClient; +import de.tum.in.www1.hephaestus.intelligenceservice.api.MentorApi; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import de.tum.in.www1.hephaestus.intelligenceservice.ApiClient; -import de.tum.in.www1.hephaestus.intelligenceservice.api.MentorApi; - @Configuration public class IntelligenceServiceConfig { @@ -19,6 +18,7 @@ public IntelligenceServiceApi intelligenceServiceApi() { } public class IntelligenceServiceApi extends MentorApi { + public IntelligenceServiceApi() { super(new ApiClient().setBasePath(intelligenceServiceUrl)); } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/KeycloakConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/KeycloakConfig.java index 9eea4f0d..78af739c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/KeycloakConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/KeycloakConfig.java @@ -1,11 +1,11 @@ package de.tum.in.www1.hephaestus.config; + import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.beans.factory.annotation.Value; - @Configuration public class KeycloakConfig { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/NatsConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/NatsConfig.java index 6ba65f46..6305a70c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/NatsConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/NatsConfig.java @@ -1,21 +1,20 @@ package de.tum.in.www1.hephaestus.config; +import io.nats.client.Connection; +import io.nats.client.Nats; +import io.nats.client.Options; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import io.nats.client.Connection; -import io.nats.client.Nats; -import io.nats.client.Options; - @Configuration public class NatsConfig { @Value("${nats.enabled}") private boolean isNatsEnabled; - + @Value("${nats.server}") private String natsServer; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SentryConfiguration.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SentryConfiguration.java index 104a0ec7..231b4d62 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SentryConfiguration.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SentryConfiguration.java @@ -49,7 +49,7 @@ public void init() { options.setRelease(hephaestusVersion); options.setTracesSampleRate(getTracesSampleRate()); }); - + logger.info("Sentry configuration was successful"); } catch (Exception ex) { logger.error("Sentry configuration was not successful due to exception!", ex); diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SlackAppConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SlackAppConfig.java index 98596fc4..56d0d892 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SlackAppConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SlackAppConfig.java @@ -1,14 +1,14 @@ package de.tum.in.www1.hephaestus.config; +import com.slack.api.bolt.App; +import com.slack.api.bolt.AppConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.slack.api.bolt.App; -import com.slack.api.bolt.AppConfig; - @Configuration public class SlackAppConfig { + @Value("${slack.token}") private String botToken; @@ -22,4 +22,4 @@ public App initSlackApp() { } return new App(AppConfig.builder().singleTeamBotToken(botToken).signingSecret(signingSecret).build()); } -} \ No newline at end of file +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SpringAsyncConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SpringAsyncConfig.java index 844e4789..63125b70 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SpringAsyncConfig.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/config/SpringAsyncConfig.java @@ -5,4 +5,4 @@ @Configuration @EnableAsync -public class SpringAsyncConfig { } \ No newline at end of file +public class SpringAsyncConfig {} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/AccessForbiddenException.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/AccessForbiddenException.java index b9e69b9d..d77565da 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/AccessForbiddenException.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/AccessForbiddenException.java @@ -1,7 +1,6 @@ package de.tum.in.www1.hephaestus.core.exception; import java.io.Serial; - import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/EntityNotFoundException.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/EntityNotFoundException.java index 8c2b34fa..509a8f5f 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/EntityNotFoundException.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/core/exception/EntityNotFoundException.java @@ -1,7 +1,6 @@ package de.tum.in.www1.hephaestus.core.exception; import java.io.Serial; - import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/AuthorAssociation.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/AuthorAssociation.java index a8f6be60..3540c6ae 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/AuthorAssociation.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/AuthorAssociation.java @@ -9,5 +9,5 @@ public enum AuthorAssociation { MANNEQUIN, MEMBER, NONE, - OWNER + OWNER, } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntity.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntity.java index d739c520..0d4ab458 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntity.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntity.java @@ -1,9 +1,8 @@ package de.tum.in.www1.hephaestus.gitprovider.common; -import java.time.OffsetDateTime; - import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; +import java.time.OffsetDateTime; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,6 +16,7 @@ @AllArgsConstructor @ToString public abstract class BaseGitServiceEntity { + @Id protected Long id; diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntityConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntityConverter.java index c286f0b3..ca5b85cc 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntityConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/BaseGitServiceEntityConverter.java @@ -1,21 +1,20 @@ package de.tum.in.www1.hephaestus.gitprovider.common; import java.io.IOException; - import org.kohsuke.github.GHObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.lang.NonNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @ReadingConverter public abstract class BaseGitServiceEntityConverter - implements Converter { + implements Converter { private static final Logger logger = LoggerFactory.getLogger(BaseGitServiceEntityConverter.class); - abstract public T update(@NonNull S source, @NonNull T target); + public abstract T update(@NonNull S source, @NonNull T target); protected void convertBaseFields(S source, T target) { if (source == null || target == null) { @@ -39,4 +38,4 @@ protected void convertBaseFields(S source, T target) { target.setUpdatedAt(null); } } -} \ No newline at end of file +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/DateUtil.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/DateUtil.java index 41b49feb..171bf196 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/DateUtil.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/DateUtil.java @@ -5,6 +5,7 @@ import java.util.Date; public class DateUtil { + public static OffsetDateTime convertToOffsetDateTime(Date date) { return date != null ? date.toInstant().atOffset(ZoneOffset.UTC) : null; } diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java index 1b94800a..445cc7ef 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubAuthorAssociationConverter.java @@ -1,5 +1,6 @@ package de.tum.in.www1.hephaestus.gitprovider.common.github; +import de.tum.in.www1.hephaestus.gitprovider.common.AuthorAssociation; import org.kohsuke.github.GHCommentAuthorAssociation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -7,8 +8,6 @@ import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; -import de.tum.in.www1.hephaestus.gitprovider.common.AuthorAssociation; - @Component public class GitHubAuthorAssociationConverter implements Converter { diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandler.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandler.java index 8600be8e..37cd9429 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandler.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandler.java @@ -1,17 +1,15 @@ package de.tum.in.www1.hephaestus.gitprovider.common.github; +import io.nats.client.Message; +import io.nats.client.MessageHandler; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; - import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHEventPayload; import org.kohsuke.github.GitHub; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import io.nats.client.Message; -import io.nats.client.MessageHandler; import org.springframework.stereotype.Component; @Component diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandlerRegistry.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandlerRegistry.java index 35126190..b3a79d6c 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandlerRegistry.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandlerRegistry.java @@ -4,7 +4,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.kohsuke.github.GHEvent; import org.springframework.stereotype.Component; @@ -26,4 +25,4 @@ public GitHubMessageHandler getHandler(GHEvent eventType) { public List getSupportedEvents() { return new ArrayList<>(handlerMap.keySet()); } -} \ No newline at end of file +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/Issue.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/Issue.java index beb396c9..c281d110 100644 --- a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/Issue.java +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/issue/Issue.java @@ -1,16 +1,15 @@ package de.tum.in.www1.hephaestus.gitprovider.issue; -import java.util.HashSet; -import java.util.Set; -import java.time.OffsetDateTime; - -import org.springframework.lang.NonNull; - -import jakarta.persistence.Table; +import de.tum.in.www1.hephaestus.gitprovider.common.BaseGitServiceEntity; +import de.tum.in.www1.hephaestus.gitprovider.issuecomment.IssueComment; +import de.tum.in.www1.hephaestus.gitprovider.label.Label; +import de.tum.in.www1.hephaestus.gitprovider.milestone.Milestone; +import de.tum.in.www1.hephaestus.gitprovider.repository.Repository; +import de.tum.in.www1.hephaestus.gitprovider.user.User; import jakarta.persistence.CascadeType; import jakarta.persistence.DiscriminatorColumn; -import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -23,17 +22,16 @@ import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.HashSet; +import java.util.Set; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.experimental.Accessors; -import de.tum.in.www1.hephaestus.gitprovider.common.BaseGitServiceEntity; -import de.tum.in.www1.hephaestus.gitprovider.issuecomment.IssueComment; -import de.tum.in.www1.hephaestus.gitprovider.label.Label; -import de.tum.in.www1.hephaestus.gitprovider.milestone.Milestone; -import de.tum.in.www1.hephaestus.gitprovider.repository.Repository; -import de.tum.in.www1.hephaestus.gitprovider.user.User; +import org.springframework.lang.NonNull; @Entity @Table(name = "issue") @@ -59,13 +57,13 @@ public class Issue extends BaseGitServiceEntity { @ToString.Exclude private String body; - @NonNull + @NonNull private String htmlUrl; private boolean isLocked; - + private OffsetDateTime closedAt; - + private int commentsCount; @Accessors(prefix = "") @@ -73,19 +71,27 @@ public class Issue extends BaseGitServiceEntity { // The last time the issue and its associated comments were updated (is also used for pull requests with reviews and review comments) private OffsetDateTime lastSyncAt; - + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id") @ToString.Exclude private User author; - + @ManyToMany - @JoinTable(name = "issue_label", joinColumns = @JoinColumn(name = "issue_id"), inverseJoinColumns = @JoinColumn(name = "label_id")) + @JoinTable( + name = "issue_label", + joinColumns = @JoinColumn(name = "issue_id"), + inverseJoinColumns = @JoinColumn(name = "label_id") + ) @ToString.Exclude private Set
+

Leagues

+ diff --git a/webapp/src/app/workspace/workspace.component.ts b/webapp/src/app/workspace/workspace.component.ts index 189b58f8..c78ca301 100644 --- a/webapp/src/app/workspace/workspace.component.ts +++ b/webapp/src/app/workspace/workspace.component.ts @@ -86,4 +86,9 @@ export class WorkspaceComponent { }, onSuccess: () => this.queryClient.invalidateQueries({ queryKey: ['workspace', 'repositoriesToMonitor'] }) })); + + resetAndRecalculateLeagues = injectMutation(() => ({ + mutationFn: () => lastValueFrom(this.workspaceService.resetAndRecalculateLeagues()), + onSuccess: () => this.queryClient.invalidateQueries({ queryKey: ['workspace', 'resetAndRecalculateLeagues'] }) + })); } From 1553fe4a29c50b5ba146c188884e5b6d9fe308a6 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sat, 4 Jan 2025 15:12:08 +0100 Subject: [PATCH 17/22] Fetch own leaguepoints --- webapp/src/app/home/home.component.html | 2 +- webapp/src/app/home/home.component.ts | 10 +++++++++- .../src/app/ui/league/elo-card/elo-card.component.ts | 12 +++--------- .../src/app/ui/league/icon/league-icon.component.ts | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/webapp/src/app/home/home.component.html b/webapp/src/app/home/home.component.html index 84d2953f..4914e7ad 100644 --- a/webapp/src/app/home/home.component.html +++ b/webapp/src/app/home/home.component.html @@ -18,7 +18,7 @@

Something went wrong...

We couldn't load the leaderboard. Please try again later.

} @else { - +
diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts index b72881b6..7f13b5a2 100644 --- a/webapp/src/app/home/home.component.ts +++ b/webapp/src/app/home/home.component.ts @@ -11,7 +11,7 @@ import { LeaderboardComponent } from '@app/home/leaderboard/leaderboard.componen import { LeaderboardFilterComponent } from './leaderboard/filter/filter.component'; import { SecurityStore } from '@app/core/security/security-store.service'; import { HlmAlertModule } from '@spartan-ng/ui-alert-helm'; -import { MetaService } from '@app/core/modules/openapi'; +import { MetaService, UserService } from '@app/core/modules/openapi'; import { LeaderboardLegendComponent } from './leaderboard/legend/legends.component'; import { LeaderboardLeagueComponent } from './leaderboard/league/league.component'; @@ -29,6 +29,7 @@ export class HomeComponent { securityStore = inject(SecurityStore); metaService = inject(MetaService); leaderboardService = inject(LeaderboardService); + userService = inject(UserService); signedIn = this.securityStore.signedIn; user = this.securityStore.loadedUser; @@ -60,4 +61,11 @@ export class HomeComponent { queryKey: ['meta'], queryFn: async () => lastValueFrom(this.metaService.getMetaData()) })); + + + userMeQuery = injectQuery(() => ({ + enabled: !!this.user(), + queryKey: ['user', { id: this.user()?.username }], + queryFn: async () => lastValueFrom(this.userService.getUserProfile(this.user()!.username)) + })); } diff --git a/webapp/src/app/ui/league/elo-card/elo-card.component.ts b/webapp/src/app/ui/league/elo-card/elo-card.component.ts index 927e0f72..965f8559 100644 --- a/webapp/src/app/ui/league/elo-card/elo-card.component.ts +++ b/webapp/src/app/ui/league/elo-card/elo-card.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, input, signal, OnInit } from '@angular/core'; +import { Component, computed, input } from '@angular/core'; import { HlmCardModule } from '@spartan-ng/ui-card-helm'; import { BrnProgressComponent, BrnProgressIndicatorComponent } from '@spartan-ng/ui-progress-brain'; import { HlmProgressIndicatorDirective } from '@spartan-ng/ui-progress-helm'; @@ -13,18 +13,12 @@ import { LucideAngularModule, Star } from 'lucide-angular'; imports: [HlmCardModule, LeagueIconComponent, BrnProgressComponent, BrnProgressIndicatorComponent, HlmProgressIndicatorDirective, HlmButtonModule, LucideAngularModule], templateUrl: './elo-card.component.html' }) -export class LeagueEloCardComponent implements OnInit { +export class LeagueEloCardComponent { protected Star = Star; protected Infinity = Infinity; leaguePoints = input(); currentLeague = computed(() => getLeagueFromPoints(this.leaguePoints()!)); - progressValue = signal(0); - - ngOnInit() { - if (this.leaguePoints() && this.currentLeague()) { - this.progressValue.set(((this.leaguePoints()! - this.currentLeague()!.minPoints) * 100) / (this.currentLeague()!.maxPoints - this.currentLeague()!.minPoints)); - } - } + progressValue = computed(() => ((this.leaguePoints()! - this.currentLeague()!.minPoints) * 100) / (this.currentLeague()!.maxPoints - this.currentLeague()!.minPoints)); } diff --git a/webapp/src/app/ui/league/icon/league-icon.component.ts b/webapp/src/app/ui/league/icon/league-icon.component.ts index 894ea258..6c8ddbc2 100644 --- a/webapp/src/app/ui/league/icon/league-icon.component.ts +++ b/webapp/src/app/ui/league/icon/league-icon.component.ts @@ -33,7 +33,7 @@ type LeagueVariants = VariantProps; selector: 'app-icon-league', standalone: true, imports: [LucideAngularModule], - template: ` ` + template: ` ` }) export class LeagueIconComponent { protected Crown = Crown; From ad26cbe910f5a4a2923e80c09835d308095a0f90 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sat, 4 Jan 2025 20:57:33 +0100 Subject: [PATCH 18/22] chore: prettier format --- webapp/src/app/home/home.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts index 7f13b5a2..35866532 100644 --- a/webapp/src/app/home/home.component.ts +++ b/webapp/src/app/home/home.component.ts @@ -62,7 +62,6 @@ export class HomeComponent { queryFn: async () => lastValueFrom(this.metaService.getMetaData()) })); - userMeQuery = injectQuery(() => ({ enabled: !!this.user(), queryKey: ['user', { id: this.user()?.username }], From 532b6f3a2bce95116cdae545427544505c48eae5 Mon Sep 17 00:00:00 2001 From: GODrums Date: Sat, 4 Jan 2025 21:29:33 +0100 Subject: [PATCH 19/22] Remove reviewing button --- .../src/app/home/leaderboard/league/league.component.html | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/webapp/src/app/home/leaderboard/league/league.component.html b/webapp/src/app/home/leaderboard/league/league.component.html index 59e76ce6..2961f0ce 100644 --- a/webapp/src/app/home/leaderboard/league/league.component.html +++ b/webapp/src/app/home/leaderboard/league/league.component.html @@ -4,13 +4,8 @@

Your League

-
+
From bdf6f8c0d5a8c12ae3897e021233fcaf57ac066e Mon Sep 17 00:00:00 2001 From: GODrums Date: Wed, 8 Jan 2025 11:37:01 +0100 Subject: [PATCH 20/22] Replace crown with star --- webapp/src/app/ui/league/icon/league-icon.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/app/ui/league/icon/league-icon.component.ts b/webapp/src/app/ui/league/icon/league-icon.component.ts index 6c8ddbc2..e829c2c7 100644 --- a/webapp/src/app/ui/league/icon/league-icon.component.ts +++ b/webapp/src/app/ui/league/icon/league-icon.component.ts @@ -1,6 +1,6 @@ import { Component, computed, input } from '@angular/core'; import { cn, getLeagueFromPoints } from '@app/utils'; -import { LucideAngularModule, Crown } from 'lucide-angular'; +import { LucideAngularModule, Medal } from 'lucide-angular'; import { type VariantProps, cva } from 'class-variance-authority'; export const leagueVariants = cva('size-8', { @@ -33,10 +33,10 @@ type LeagueVariants = VariantProps; selector: 'app-icon-league', standalone: true, imports: [LucideAngularModule], - template: ` ` + template: ` ` }) export class LeagueIconComponent { - protected Crown = Crown; + protected Medal = Medal; size = input('default'); leaguePoints = input(); From fda38b86c90ff3cf551b39bad85a868c929617bf Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 20 Jan 2025 22:10:36 +0100 Subject: [PATCH 21/22] fix loading --- webapp/src/app/home/leaderboard/leaderboard.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/app/home/leaderboard/leaderboard.component.html b/webapp/src/app/home/leaderboard/leaderboard.component.html index eb01e17d..fe6975de 100644 --- a/webapp/src/app/home/leaderboard/leaderboard.component.html +++ b/webapp/src/app/home/leaderboard/leaderboard.component.html @@ -15,11 +15,11 @@ @if (isLoading()) { @for (entry of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track entry; let idx = $index) { - + - - + + From 2f97d006f60613ffdf697da1348505a2aa32a197 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 20 Jan 2025 22:15:38 +0100 Subject: [PATCH 22/22] other fixes --- webapp/src/app/home/leaderboard/legend/legend.component.html | 2 +- .../src/libs/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/app/home/leaderboard/legend/legend.component.html b/webapp/src/app/home/leaderboard/legend/legend.component.html index 05667b27..43871961 100644 --- a/webapp/src/app/home/leaderboard/legend/legend.component.html +++ b/webapp/src/app/home/leaderboard/legend/legend.component.html @@ -1,5 +1,5 @@
-
+

Icons

diff --git a/webapp/src/libs/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts b/webapp/src/libs/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts index 74e049c7..6dd527a8 100644 --- a/webapp/src/libs/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts +++ b/webapp/src/libs/ui/ui-dialog-helm/src/lib/hlm-dialog.component.ts @@ -5,7 +5,7 @@ import { HlmDialogOverlayDirective } from './hlm-dialog-overlay.directive'; @Component({ selector: 'hlm-dialog', standalone: true, - imports: [BrnDialogComponent, BrnDialogOverlayComponent, HlmDialogOverlayDirective], + imports: [BrnDialogOverlayComponent, HlmDialogOverlayDirective], providers: [ { provide: BrnDialogComponent,