Repository: the-blue-alliance/the-blue-alliance
Pull request #7356: Add 2 new insights
Author: login: tervay
Created: 2025-03-13T21:07:23Z | Merged: 2025-03-14T08:10:22Z

1. Adds DCMP winner notable
2. Adds longest Einstein streak leaderboard (does not support multiple streaks currently)

idk why it wants me to reformat those other files but it does


Diff:
diff --git a/src/backend/common/helpers/insights_leaderboard_team_helper.py b/src/backend/common/helpers/insights_leaderboard_team_helper.py
index 762c0fcad8..d6a9b7a736 100644
--- a/src/backend/common/helpers/insights_leaderboard_team_helper.py
+++ b/src/backend/common/helpers/insights_leaderboard_team_helper.py
@@ -1,14 +1,15 @@
 from collections import defaultdict
-from typing import List, Optional
+from typing import DefaultDict, List, Optional
 
 from backend.common.consts.award_type import AwardType, BLUE_BANNER_AWARDS
-from backend.common.consts.event_type import NON_CMP_EVENT_TYPES
+from backend.common.consts.event_type import EventType, NON_CMP_EVENT_TYPES
 from backend.common.helpers.insights_helper_utils import (
     LeaderboardInsightArguments,
     make_insights_from_functions,
     make_leaderboard_from_dict_counts,
 )
 from backend.common.models.insight import Insight
+from backend.common.models.keys import Year
 
 
 class InsightsLeaderboardTeamHelper:
@@ -23,6 +24,7 @@ def make_insights(year: int) -> List[Insight]:
                 InsightsLeaderboardTeamHelper._most_matches_played,
                 InsightsLeaderboardTeamHelper._most_events_played_at,
                 InsightsLeaderboardTeamHelper._most_unique_teams_played_with_or_against,
+                InsightsLeaderboardTeamHelper._longest_einstein_streak,
             ],
         )
 
@@ -123,3 +125,58 @@ def _most_unique_teams_played_with_or_against(
             Insight.TYPED_LEADERBOARD_MOST_UNIQUE_TEAMS_PLAYED_WITH_AGAINST,
             arguments.year,
         )
+
+    @staticmethod
+    def _longest_einstein_streak(
+        arguments: LeaderboardInsightArguments,
+    ) -> Optional[Insight]:
+        if arguments.year != 0:
+            return None
+
+        einstein_appearances: DefaultDict[str, List[int]] = defaultdict(list)
+
+        for award in arguments.awards():
+            if (
+                award.award_type_enum == AwardType.WINNER
+                and award.event_type_enum == EventType.CMP_DIVISION
+            ):
+                for team_key in award.team_list:
+                    year = int(str(award.event.string_id())[:4])
+                    einstein_appearances[str(team_key.string_id())].append(year)
+
+        def are_years_consecutive(a: Year, b: Year) -> bool:
+            # 2020, 2021 divisions didn't happen because COVID
+            if a in [2019, 2022] and b in [2019, 2022] and a != b:
+                return True
+
+            return a == b + 1 or a == b - 1
+
+        def get_streaks(appearances: List[int]) -> List[int]:
+            streaks = []
+            current_streak = 0
+
+            for i, appearance in enumerate(appearances):
+                if current_streak == 0:
+                    current_streak += 1
+                else:
+                    if are_years_consecutive(appearance, appearances[i - 1]):
+                        current_streak += 1
+                    else:
+                        streaks.append(current_streak)
+                        current_streak = 1
+
+            streaks.append(current_streak)
+            return streaks
+
+        streaks = {
+            tk: get_streaks(appearances)
+            for tk, appearances in einstein_appearances.items()
+        }
+
+        longest_streaks = {tk: max(streaks) for tk, streaks in streaks.items()}
+
+        return make_leaderboard_from_dict_counts(
+            longest_streaks,
+            Insight.TYPED_LEADERBOARD_LONGEST_EINSTEIN_STREAK,
+            arguments.year,
+        )
diff --git a/src/backend/common/helpers/insights_notable_helper.py b/src/backend/common/helpers/insights_notable_helper.py
index a61e238738..e358c8ef4d 100644
--- a/src/backend/common/helpers/insights_notable_helper.py
+++ b/src/backend/common/helpers/insights_notable_helper.py
@@ -26,6 +26,7 @@ def make_insights(year: Year) -> List[Insight]:
                 InsightsNotableHelper._calculate_notables_world_champions,
                 InsightsNotableHelper._calculate_notables_division_winners,
                 InsightsNotableHelper._calculate_notables_division_finals_appearances,
+                InsightsNotableHelper._calculate_notables_dcmp_winner,
             ],
         )
 
@@ -129,3 +130,14 @@ def _calculate_notables_division_finals_appearances(
             Insight.TYPED_NOTABLES_DIVISION_FINALS_APPEARANCES,
             arguments.year,
         )
+
+    @staticmethod
+    def _calculate_notables_dcmp_winner(
+        arguments: LeaderboardInsightArguments,
+    ) -> Insight:
+        return InsightsNotableHelper.__make_notable_from_award_type_at_event_type(
+            arguments,
+            AwardType.WINNER,
+            EventType.DISTRICT_CMP,
+            Insight.TYPED_NOTABLES_DCMP_WINNER,
+        )
diff --git a/src/backend/common/helpers/tests/insights_leaderboard_team_helper_test.py b/src/backend/common/helpers/tests/insights_leaderboard_team_helper_test.py
index e6380a7cf7..8ded070579 100644
--- a/src/backend/common/helpers/tests/insights_leaderboard_team_helper_test.py
+++ b/src/backend/common/helpers/tests/insights_leaderboard_team_helper_test.py
@@ -1,9 +1,15 @@
+from google.appengine.ext import ndb
+
+from backend.common.consts.award_type import AwardType
+from backend.common.consts.event_type import EventType
 from backend.common.helpers.insights_leaderboard_team_helper import (
     InsightsLeaderboardTeamHelper,
     LeaderboardInsightArguments,
 )
+from backend.common.models.award import Award
 from backend.common.models.event import Event
 from backend.common.models.insight import Insight
+from backend.common.models.team import Team
 
 
 def test_most_blue_banners(ndb_stub, test_data_importer):
@@ -227,3 +233,69 @@ def test_only_official_events_are_included(ndb_stub, test_data_importer):
         },
         {"keys": ["frc2344"], "value": 13},
     ]
+
+
+def test_longest_einstein_streak_across_covid(ndb_stub):
+    Team(id="frc254", team_number=254).put()
+    Event(
+        id="2018hop",
+        year=2018,
+        event_short="hop",
+        event_type_enum=EventType.CMP_DIVISION,
+    ).put()
+    Award(
+        id="2018hop_1",
+        year=2018,
+        award_type_enum=AwardType.WINNER,
+        event_type_enum=EventType.CMP_DIVISION,
+        event=ndb.Key(Event, "2018hop"),
+        name_str="Winner",
+        team_list=[ndb.Key(Team, "frc254")],
+    ).put()
+    Event(
+        id="2019tur",
+        year=2019,
+        event_short="tur",
+        event_type_enum=EventType.CMP_DIVISION,
+    ).put()
+    Award(
+        id="2019tur_1",
+        year=2019,
+        award_type_enum=AwardType.WINNER,
+        event_type_enum=EventType.CMP_DIVISION,
+        event=ndb.Key(Event, "2019tur"),
+        name_str="Winner",
+        team_list=[ndb.Key(Team, "frc254")],
+    ).put()
+    Event(
+        id="2022gal",
+        year=2022,
+        event_short="gal",
+        event_type_enum=EventType.CMP_DIVISION,
+    ).put()
+    Award(
+        id="2022gal_1",
+        year=2022,
+        award_type_enum=AwardType.WINNER,
+        event_type_enum=EventType.CMP_DIVISION,
+        event=ndb.Key(Event, "2022gal"),
+        name_str="Winner",
+        team_list=[ndb.Key(Team, "frc254")],
+    ).put()
+
+    insight = InsightsLeaderboardTeamHelper._longest_einstein_streak(
+        LeaderboardInsightArguments(
+            events=[
+                Event.get_by_id("2018hop"),
+                Event.get_by_id("2019tur"),
+                Event.get_by_id("2022gal"),
+            ],
+            year=0,
+        )
+    )
+
+    assert insight is not None
+    assert insight.data["rankings"][:3] == [
+        {"keys": ["frc254"], "value": 3},
+    ]
+    assert insight.data["key_type"] == "team"
diff --git a/src/backend/common/helpers/tests/insights_notable_helper_test.py b/src/backend/common/helpers/tests/insights_notable_helper_test.py
index d416106477..6796a6d26e 100644
--- a/src/backend/common/helpers/tests/insights_notable_helper_test.py
+++ b/src/backend/common/helpers/tests/insights_notable_helper_test.py
@@ -143,3 +143,85 @@ def test_notables_division_finals_appearances(ndb_stub):
         {"team_key": "frc604", "context": ["2024mil"]},
         {"team_key": "frc2713", "context": ["2024mil"]},
     ]
+
+
+def test_notables_dcmp_winner(ndb_stub):
+    # Set up DCMPs
+    events = []
+    for event_short, years in [
+        ("necmp", [2018, 2019, 2020, 2021, 2022]),
+        ("micmp", [2018, 2019, 2020, 2021, 2022]),
+    ]:
+        for year in years:
+            Event(
+                id=f"{year}{event_short}",
+                year=year,
+                event_short=event_short,
+                event_type_enum=EventType.DISTRICT_CMP,
+            ).put()
+            events.append(Event.get_by_id(f"{year}{event_short}"))
+
+            Award(
+                id=f"{year}{event_short}_winner",
+                year=year,
+                award_type_enum=AwardType.WINNER,
+                event_type_enum=EventType.DISTRICT_CMP,
+                event=ndb.Key(Event, f"{year}{event_short}"),
+                name_str="Winner",
+                team_list=[
+                    ndb.Key(Team, "frc2713" if event_short == "necmp" else "frc1")
+                ],
+            ).put()
+
+            # Set up divisions to make sure we don't count them
+            for div in range(1, 3):
+                Event(
+                    id=f"{year}{event_short}{div}",
+                    year=year,
+                    event_short=f"{event_short}{div}",
+                    event_type_enum=EventType.DISTRICT_CMP_DIVISION,
+                ).put()
+                events.append(Event.get_by_id(f"{year}{event_short}{div}"))
+
+                Award(
+                    id=f"{year}{event_short}{div}_winner",
+                    year=year,
+                    award_type_enum=AwardType.WINNER,
+                    event_type_enum=EventType.DISTRICT_CMP_DIVISION,
+                    event=ndb.Key(Event, f"{year}{event_short}{div}"),
+                    name_str="Winner",
+                    team_list=[
+                        ndb.Key(Team, "frc2713" if event_short == "necmp" else "frc1")
+                    ],
+                ).put()
+
+    insight = InsightsNotableHelper._calculate_notables_dcmp_winner(
+        LeaderboardInsightArguments(
+            events=events,
+            year=2024,
+        )
+    )
+
+    assert insight is not None
+    assert insight.data["entries"] == [
+        {
+            "team_key": "frc2713",
+            "context": [
+                "2018necmp",
+                "2019necmp",
+                "2020necmp",
+                "2021necmp",
+                "2022necmp",
+            ],
+        },
+        {
+            "team_key": "frc1",
+            "context": [
+                "2018micmp",
+                "2019micmp",
+                "2020micmp",
+                "2021micmp",
+                "2022micmp",
+            ],
+        },
+    ]
diff --git a/src/backend/common/models/insight.py b/src/backend/common/models/insight.py
index 069ad3df4a..29b42a5730 100644
--- a/src/backend/common/models/insight.py
+++ b/src/backend/common/models/insight.py
@@ -55,6 +55,8 @@ class Insight(CachedModel):
     TYPED_NOTABLES_HALL_OF_FAME = 34
     TYPED_LEADERBOARD_MOST_EVENTS_PLAYED_AT = 35
     TYPED_LEADERBOARD_2025_MOST_CORAL_SCORED = 36
+    TYPED_LEADERBOARD_LONGEST_EINSTEIN_STREAK = 37
+    TYPED_NOTABLES_DCMP_WINNER = 35
     YEAR_SPECIFIC_BY_WEEK = 999
     YEAR_SPECIFIC = 1000
 
@@ -99,6 +101,8 @@ class Insight(CachedModel):
         YEAR_SPECIFIC_BY_WEEK: "year_specific_by_week",
         YEAR_SPECIFIC: "year_specific",
         TYPED_LEADERBOARD_2025_MOST_CORAL_SCORED: "typed_leaderboard_2025_most_coral_scored",
+        TYPED_LEADERBOARD_LONGEST_EINSTEIN_STREAK: "typed_leaderboard_longest_einstein_streak",
+        TYPED_NOTABLES_DCMP_WINNER: "notables_dcmp_winner",
     }
 
     TYPED_LEADERBOARD_KEY_TYPES: Dict[int, LeaderboardKeyType] = {
@@ -112,6 +116,7 @@ class Insight(CachedModel):
         TYPED_LEADERBOARD_MOST_UNIQUE_TEAMS_PLAYED_WITH_AGAINST: "team",
         TYPED_LEADERBOARD_MOST_EVENTS_PLAYED_AT: "team",
         TYPED_LEADERBOARD_2025_MOST_CORAL_SCORED: "match",
+        TYPED_LEADERBOARD_LONGEST_EINSTEIN_STREAK: "team",
     }
 
     NOTABLE_INSIGHTS = {
@@ -119,6 +124,7 @@ class Insight(CachedModel):
         TYPED_NOTABLES_DIVISION_FINALS_APPEARANCES,
         TYPED_NOTABLES_WORLD_CHAMPIONS,
         TYPED_NOTABLES_HALL_OF_FAME,
+        TYPED_NOTABLES_DCMP_WINNER,
     }
 
     name = ndb.StringProperty(required=True)  # general name used for sorting


Reviews:
user: login: fangeugene
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T21:12:33Z
user: login: tervay
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T21:12:58Z
user: login: fangeugene
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T02:13:40Z

Review comments:
diff_hunk: @@ -541,26 +541,18 @@ def test_merge_models() -> None:
 
 
 def test_update_manual_attrs() -> None:
-    old = DummyModel(id="test", int_prop=42, manual_attrs=['int_prop'])
+    old = DummyModel(id="test", int_prop=42, manual_attrs=["int_prop"])
path: src/backend/common/manipulators/tests/base_manipulator_test.py
user: login: fangeugene
body: why is this suddenly a problem lol
created_at: 2025-03-13T21:12:33Z
author_association: MEMBER
diff_hunk: @@ -541,26 +541,18 @@ def test_merge_models() -> None:
 
 
 def test_update_manual_attrs() -> None:
-    old = DummyModel(id="test", int_prop=42, manual_attrs=['int_prop'])
+    old = DummyModel(id="test", int_prop=42, manual_attrs=["int_prop"])
path: src/backend/common/manipulators/tests/base_manipulator_test.py
user: login: tervay
body: no idea
created_at: 2025-03-13T21:12:58Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-13T21:08:54Z
body: ## [Codecov](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/pull/7356?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance) Report
Attention: Patch coverage is `97.05882%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 77.62%. Comparing base [(`9e26664`)](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/commit/9e266648dbbf8c7a0739ede391fb8fe5c4ce3faa?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance) to head [(`4e3a651`)](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/commit/4e3a6514d2e4e2af294542324d62b5fd1610882d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance).
> Report is 1 commits behind head on py3.

| [Files with missing lines](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/pull/7356?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance) | Patch % | Lines |
|---|---|---|
| [...common/helpers/insights\_leaderboard\_team\_helper.py](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/pull/7356?src=pr&el=tree&filepath=src%2Fbackend%2Fcommon%2Fhelpers%2Finsights_leaderboard_team_helper.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance#diff-c3JjL2JhY2tlbmQvY29tbW9uL2hlbHBlcnMvaW5zaWdodHNfbGVhZGVyYm9hcmRfdGVhbV9oZWxwZXIucHk=) | 93.75% | [2 Missing :warning: ](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/pull/7356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              py3    #7356      +/-   ##
==========================================
+ Coverage   77.60%   77.62%   +0.02%     
==========================================
  Files         789      789              
  Lines       54510    54576      +66     
  Branches      220      220              
==========================================
+ Hits        42301    42365      +64     
- Misses      12135    12137       +2     
  Partials       74       74              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/the-blue-alliance/the-blue-alliance/pull/7356?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=the-blue-alliance).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-13T21:26:51Z
body: ## Screenshots

### Homepage
![Homepage](https://github.com/the-blue-alliance/the-blue-alliance/raw/ci-screenshots/pr-7356-http:--localhost:8080-1741901921.png)

### GameDay
![GameDay](https://github.com/the-blue-alliance/the-blue-alliance/raw/ci-screenshots/pr-7356-http:--localhost:8080-gameday-1741901927.png)


<hidden purpose="for-rewritable-pr-comment-action-use" value="screenshots-comment-rewritable-action"></hidden>
author_association: CONTRIBUTOR