diff --git a/README.md b/README.md index 2c64dbbe0..3293e3cf0 100644 --- a/README.md +++ b/README.md @@ -49,35 +49,35 @@ _Last updated: 02/14/2026_ This repository is a monorepo that contains many different pieces of Codebloom, which include: - Our public task board on [Notion](https://www.notion.so/) which is used to define a relationship between tasks and PRs - - [View public taskboard](https://codebloom.notion.site/) - - [Goto `.github/scripts/notion/index.ts` to view custom PR/commit check validation implementation](./.github/scripts/notion/index.ts) + - [View public taskboard](https://codebloom.notion.site/) + - [Goto `.github/scripts/notion/index.ts` to view custom PR/commit check validation implementation](./.github/scripts/notion/index.ts) - Our backend which is written in [Java 25](https://dev.java/) and [Spring Boot 3](https://spring.io/projects/spring-boot). The backend interfaces with our database via [pgJDBC](https://jdbc.postgresql.org/), Discord via [JDA](https://jda.wiki/), [leetcode.com](https://leetcode.com) via a custom abstraction over their [GraphQL](https://graphql.org/) API as well as [Playwright](https://playwright.dev/java/) to obtain credentials to [leetcode.com](https://leetcode.com) via [GitHub OAuth](https://github.com). - - [Goto `src/`](./src/) - - [View detailed documentation](./src/README.md) - - [View OpenAPI schema](https://codebloom.patinanetwork.org/swagger-ui/index.html) + - [Goto `src/`](./src/) + - [View detailed documentation](./src/README.md) + - [View OpenAPI schema](https://codebloom.patinanetwork.org/swagger-ui/index.html) - Our main database is [PostgreSQL 16](https://www.postgresql.org/), which is controlled with [Flyway](https://www.red-gate.com/products/flyway/community/) migrations. - - [Goto `db/`](./db/) - - [View detailed documentation](./db/README.md) + - [Goto `db/`](./db/) + - [View detailed documentation](./db/README.md) - Our frontend which is written in [TypeScript](https://www.typescriptlang.org/) and [React 18](https://www.typescriptlang.org/) + [Vite](https://vite.dev/). - - [Goto `js/`](./js/) - - [View detailed documentation](./js/README.md) + - [Goto `js/`](./js/) + - [View detailed documentation](./js/README.md) - Our email templater which utilizes [React Email](https://react.email/) and is dynamically parsed & templated at runtime with [JSoup](https://jsoup.org/) in our backend. - - [Goto `email/`](./email/) - - [View detailed documentation](./email/README.md) + - [Goto `email/`](./email/) + - [View detailed documentation](./email/README.md) - Our CI/CD pipeline which utilizes [GitHub Actions](https://github.com/features/actions) with [TypeScript](https://typescriptlang.org/) + [Bun Shell](https://bun.com/docs/runtime/shell). - - [Goto `.github/`](./.github/) - - [Goto `.github/scripts/` to view all Bun Shell scripts](./.github/scripts/) - - [View detailed documentation](./.github/READMEFIRST.md) + - [Goto `.github/`](./.github/) + - [Goto `.github/scripts/` to view all Bun Shell scripts](./.github/scripts/) + - [View detailed documentation](./.github/READMEFIRST.md) - Our service gets deployed to two environments (production & staging) to our Kubernetes cluster (doc update WIP). We containerize the main application with [Docker](https://www.docker.com/) and monitor metrics & logs with [Prometheus](https://prometheus.io/) & [OpenSearch](https://opensearch.org/) which get fed to [Grafana](https://grafana.com/). - - [Goto `.github/scripts/redeploy/` to view Bun Shell redeploy scripts](./.github/scripts/redeploy/) - - [Goto `infra/`](./infra/) - - [View detailed documentation](./infra/README.md) - - [View observability documentation](https://docs.patinanetwork.org/codebloom/observability/) + - [Goto `.github/scripts/redeploy/` to view Bun Shell redeploy scripts](./.github/scripts/redeploy/) + - [Goto `infra/`](./infra/) + - [View detailed documentation](./infra/README.md) + - [View observability documentation](https://docs.patinanetwork.org/codebloom/observability/) - Our internal tools (e.g. custom Ink CLI scripts & our internal standup bot) are currently being written in [Rust](https://rust-lang.org/) and [TypeScript](https://typescriptlang.org) - - [Goto `internal/`](./internal/) - - [View detailed documentation](./internal/README.md) + - [Goto `internal/`](./internal/) + - [View detailed documentation](./internal/README.md) - Environment secrets live in local env files generated from Azure. - - [View detailed documentation](https://docs.patinanetwork.org/codebloom/local/SECRET/) + - [View detailed documentation](https://docs.patinanetwork.org/codebloom/local/SECRET/) ## Setup diff --git a/src/main/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardController.java b/src/main/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardController.java index 3223a4393..6ccb12c7f 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardController.java +++ b/src/main/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardController.java @@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.Optional; +import java.util.Set; import org.patinanetwork.codebloom.common.components.LeaderboardManager; import org.patinanetwork.codebloom.common.db.models.leaderboard.Leaderboard; import org.patinanetwork.codebloom.common.db.models.user.UserWithScore; @@ -99,6 +100,9 @@ public ResponseEntity>>> getLeaderbo @Parameter(description = "Discord name", example = "tahmid") @RequestParam(required = false, defaultValue = "") final String query, + @Parameter(description = "List of enabled filter tags", example = "nyu,hunter,patina") + @RequestParam(required = false) + final Set filters, @Parameter(description = "Filter for Patina users") @RequestParam(required = false, defaultValue = "false") final boolean patina, @Parameter(description = "Filter for Hunter College users") @@ -133,22 +137,31 @@ public ResponseEntity>>> getLeaderbo final int parsedPageSize = Math.min(pageSize, MAX_LEADERBOARD_PAGE_SIZE); - LeaderboardFilterOptions options = LeaderboardFilterOptions.builder() + final LeaderboardFilterOptions options; + + final Set normalizedFilters = filters == null + ? Set.of() + : filters.stream() + .filter(java.util.Objects::nonNull) + .map(s -> s.trim().toLowerCase()) + .collect(java.util.stream.Collectors.toSet()); + + options = LeaderboardFilterOptions.builder() .page(page) .pageSize(parsedPageSize) .query(query) - .patina(patina) - .hunter(hunter) - .nyu(nyu) - .baruch(baruch) - .rpi(rpi) - .gwc(gwc) - .sbu(sbu) - .ccny(ccny) - .columbia(columbia) - .cornell(cornell) - .bmcc(bmcc) - .mhcplusplus(mhcplusplus) + .patina(patina || normalizedFilters.contains("patina")) + .hunter(hunter || normalizedFilters.contains("hunter")) + .nyu(nyu || normalizedFilters.contains("nyu")) + .baruch(baruch || normalizedFilters.contains("baruch")) + .rpi(rpi || normalizedFilters.contains("rpi")) + .gwc(gwc || normalizedFilters.contains("gwc")) + .sbu(sbu || normalizedFilters.contains("sbu")) + .ccny(ccny || normalizedFilters.contains("ccny")) + .columbia(columbia || normalizedFilters.contains("columbia")) + .cornell(cornell || normalizedFilters.contains("cornell")) + .bmcc(bmcc || normalizedFilters.contains("bmcc")) + .mhcplusplus(mhcplusplus || normalizedFilters.contains("mhcplusplus")) .build(); Page> createdPage = diff --git a/src/test/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardControllerTest.java b/src/test/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardControllerTest.java index 678f4a0ed..c6a66f442 100644 --- a/src/test/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardControllerTest.java +++ b/src/test/java/org/patinanetwork/codebloom/api/leaderboard/LeaderboardControllerTest.java @@ -1,11 +1,15 @@ package org.patinanetwork.codebloom.api.leaderboard; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -57,6 +61,7 @@ void testGetLeaderboardUsersByIdMhcPlusPlus() { PAGE, PAGE_SIZE, "", + null, false, false, false, @@ -138,6 +143,7 @@ void capsPageSize() { 1, 999, "", + null, false, false, false, @@ -167,6 +173,7 @@ void passesAllFilters() { 2, 10, "tahmid", + null, true /* patina */, true /* hunter */, true /* nyu */, @@ -203,6 +210,63 @@ void passesAllFilters() { assertTrue(opts.isMhcplusplus()); } + @Test + @DisplayName("passes all filter flags through a filter tag") + void passesAllFiltersThroughFilterTag() { + leaderboardController.getLeaderboardUsersById( + LEADERBOARD_ID, + 2, + 10, + "tahmid", + Set.of( + "patina", + "hunter", + "nyu", + "baruch", + "rpi", + "gwc", + "sbu", + "ccny", + "columbia", + "cornell", + "bmcc", + "mhcplusplus"), + false /* patina */, + false /* hunter */, + false /* nyu */, + false /* baruch */, + false /* rpi */, + false /* gwc */, + false /* sbu */, + false /* ccny */, + false /* columbia */, + false /* cornell */, + false /* bmcc */, + false /* mhc++ */, + true /* globalIndex */, + null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(LeaderboardFilterOptions.class); + verify(leaderboardManager).getLeaderboardUsers(eq(LEADERBOARD_ID), captor.capture(), eq(true)); + + var opts = captor.getValue(); + assertEquals(2, opts.getPage()); + assertEquals(10, opts.getPageSize()); + assertEquals("tahmid", opts.getQuery()); + assertTrue(opts.isPatina()); + assertTrue(opts.isHunter()); + assertTrue(opts.isNyu()); + assertTrue(opts.isBaruch()); + assertTrue(opts.isRpi()); + assertTrue(opts.isGwc()); + assertTrue(opts.isSbu()); + assertTrue(opts.isCcny()); + assertTrue(opts.isColumbia()); + assertTrue(opts.isCornell()); + assertTrue(opts.isBmcc()); + assertTrue(opts.isMhcplusplus()); + } + @Test @DisplayName("returns metadata for the most recent leaderboard") void returnsCurrentMetadata() {