Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add leaderboards v1 #761

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions graphql_api/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
from .file import commit_file, file_bindable
from .flag import flag, flag_bindable
from .flag_comparison import flag_comparison, flag_comparison_bindable
from .gamification import (
badge_bindable,
gamification,
leaderboard_bindable,
leaderboard_data_bindable,
)
from .impacted_file import (
impacted_file,
impacted_file_bindable,
Expand Down Expand Up @@ -115,6 +121,7 @@
account,
okta_config,
test_results,
gamification,
]

bindables = [
Expand Down Expand Up @@ -172,4 +179,7 @@
account_bindable,
okta_config_bindable,
test_result_bindable,
leaderboard_bindable,
leaderboard_data_bindable,
badge_bindable,
]
2 changes: 2 additions & 0 deletions graphql_api/types/enums/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from .enums import (
BadgeTier,
BundleLoadTypes,
CommitErrorCode,
CommitErrorGeneralType,
CommitStatus,
CoverageLine,
GamificationMetric,
GoalOnboarding,
LoginProvider,
OrderingDirection,
Expand Down
4 changes: 4 additions & 0 deletions graphql_api/types/enums/enum_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from timeseries.models import MeasurementName

from .enums import (
BadgeTier,
BundleLoadTypes,
CoverageLine,
GamificationMetric,
GoalOnboarding,
LoginProvider,
OrderingDirection,
Expand Down Expand Up @@ -52,4 +54,6 @@
EnumType("YamlStates", YamlStates),
EnumType("BundleLoadTypes", BundleLoadTypes),
EnumType("TestResultsOrderingParameter", TestResultsOrderingParameter),
EnumType("GamificationMetric", GamificationMetric),
EnumType("BadgeTier", BadgeTier),
]
12 changes: 12 additions & 0 deletions graphql_api/types/enums/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,15 @@ class BundleLoadTypes(enum.Enum):
ENTRY = "ENTRY"
INITIAL = "INITIAL"
LAZY = "LAZY"


class GamificationMetric(enum.Enum):
PATCH_COVERAGE_AVERAGE = "PATCH_COVERAGE_AVERAGE"
CHANGE_COVERAGE_COUNT = "CHANGE_COVERAGE_COUNT"
PR_COUNT = "PR_COUNT"


class BadgeTier(enum.Enum):
GOLD = "GOLD"
SILVER = "SILVER"
BRONZE = "BRONZE"
21 changes: 21 additions & 0 deletions graphql_api/types/enums/gamification.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

"""
Aggregated metrics for different scores being tracked per user
"""
enum GamificationMetric {
"Average patch coverage percentage (a PR with all added lines covered by tests has 100% value)"
PATCH_COVERAGE_AVERAGE
"Number of test covered lines being added to the codebase in this change"
CHANGE_COVERAGE_COUNT
"Number of PRs created and merged"
PR_COUNT
}

"""
Each gamification metric has different tiers of varying prestige
"""
enum BadgeTier {
GOLD
SILVER
BRONZE
}
12 changes: 12 additions & 0 deletions graphql_api/types/gamification/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from graphql_api.helpers.ariadne import ariadne_load_local_graphql

from .gamification import (
Badge,
BadgeCollection,
Leaderboard,
badge_bindable,
leaderboard_bindable,
leaderboard_data_bindable,
)

gamification = ariadne_load_local_graphql(__file__, "gamification.graphql")
29 changes: 29 additions & 0 deletions graphql_api/types/gamification/gamification.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

"""
Returns a single user's aggregated score for a metric
"""
type LeaderboardData {
author: Owner!
value: Float!
}

"""
Visual representing of a leaderboard for a given repo
"""
type Leaderboard {
"Name of the metric"
name: GamificationMetric!
"Ordered list of (up to) top 5 contributors for this metric of the given repo"
ranking: [LeaderboardData!]!
}


"""
A singular badge for a given gamification badge for a user
"""
type Badge {
"Name of the metric"
name: GamificationMetric!
"Prestige tier of the badge for the given metric"
tier: BadgeTier!
}
243 changes: 243 additions & 0 deletions graphql_api/types/gamification/gamification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property

from ariadne import ObjectType
from django.db import connections
from django.utils.timezone import now

from codecov.db import sync_to_async
from codecov_auth.models import Owner
from graphql_api.types.enums import BadgeTier, GamificationMetric

leaderboard_data_bindable = ObjectType("LeaderboardData")
leaderboard_bindable = ObjectType("Leaderboard")
badge_bindable = ObjectType("Badge")


class LeaderboardData:
def __init__(self, ownerid: int, value: float):
self.ownerid = ownerid
self.value = value

Check warning on line 21 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L20-L21

Added lines #L20 - L21 were not covered by tests

@cached_property
def author(self):
return Owner.objects.filter(ownerid=self.ownerid).first()

Check warning on line 25 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L25

Added line #L25 was not covered by tests

@cached_property
def value(self):
self.value

Check warning on line 29 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L29

Added line #L29 was not covered by tests


class Leaderboard:
DEFAULT_RAW_QUERY = """
SELECT
owners.ownerid,
leaderboard.metric
FROM (
SELECT
p.author,
{metric_compute} AS metric
FROM owners o
JOIN repos r ON o.ownerid = r.ownerid
LEFT JOIN pulls p ON r.repoid = p.repoid
WHERE
p.author is not null
AND o.ownerid = %s
AND r.repoid = %s
AND p.state = 'merged'
AND p.updatestamp >= %s
GROUP BY
p.author
{nested_extension}
) AS leaderboard
LEFT JOIN owners ON leaderboard.author = owners.ownerid
WHERE
owners.username IS NOT null
AND leaderboard.metric > 0
ORDER BY leaderboard.metric DESC
LIMIT 5;
"""

METRIC_COMPUTES = {
GamificationMetric.PATCH_COVERAGE_AVERAGE: {
"metric_compute": "AVG((p.diff->5->>0)::FLOAT)",
"nested_extension": "HAVING AVG((p.diff->5->>0)::FLOAT) IS NOT NULL",
},
GamificationMetric.CHANGE_COVERAGE_COUNT: {
"metric_compute": "SUM(COALESCE((p.diff->2)::INT, 0))",
"nested_extension": "",
},
GamificationMetric.PR_COUNT: {
"metric_compute": "COUNT(p.author)",
"nested_extension": "",
},
}

def __init__(self, ownerid: int, repoid: int, metric: GamificationMetric):
self.ownerid = ownerid
self.repoid = repoid
self.metric = metric

Check warning on line 80 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L78-L80

Added lines #L78 - L80 were not covered by tests

self.metric_specific_query = Leaderboard.DEFAULT_RAW_QUERY.format(

Check warning on line 82 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L82

Added line #L82 was not covered by tests
**Leaderboard.METRIC_COMPUTES[metric]
)

@cached_property
def name(self):
return self.metric

Check warning on line 88 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L88

Added line #L88 was not covered by tests

@cached_property
def ranking(self):
start_date = now() - timedelta(days=30)
with connections["default"].cursor() as cursor:
cursor.execute(

Check warning on line 94 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L92-L94

Added lines #L92 - L94 were not covered by tests
self.metric_specific_query, [self.ownerid, self.repoid, start_date]
)
return [LeaderboardData(*result) for result in cursor.fetchall()]

Check warning on line 97 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L97

Added line #L97 was not covered by tests


BADGE_METRIC_TIER_MAPPINGS = {
GamificationMetric.PATCH_COVERAGE_AVERAGE: [
(100, BadgeTier.GOLD),
(90, BadgeTier.SILVER),
(80, BadgeTier.BRONZE),
],
GamificationMetric.CHANGE_COVERAGE_COUNT: [
(200, BadgeTier.GOLD),
(150, BadgeTier.SILVER),
(100, BadgeTier.BRONZE),
],
GamificationMetric.PR_COUNT: [
(20, BadgeTier.GOLD),
(15, BadgeTier.SILVER),
(10, BadgeTier.BRONZE),
],
}


@dataclass
class Badge:
name: GamificationMetric
tier: BadgeTier


class BadgeCollection:
DEFAULT_RAW_QUERY = """
SELECT
leaderboard.patch_coverage_average,
leaderboard.change_coverage_count,
leaderboard.pr_count
FROM (
SELECT
p.author,
AVG((p.diff->5->>0)::FLOAT) AS patch_coverage_average,
SUM(COALESCE((p.diff->2)::INT, 0)) AS change_coverage_count,
COUNT(p.author) AS pr_count
FROM owners o
JOIN repos r ON o.ownerid = r.ownerid
LEFT JOIN pulls p ON r.repoid = p.repoid
WHERE
p.author = {author_ownerid}
AND o.service = '{service}'
AND o.username = %s
AND r.name = %s
AND p.state = 'merged'
AND p.updatestamp >= %s
GROUP BY
p.author
) AS leaderboard
LEFT JOIN owners ON leaderboard.author = owners.ownerid
WHERE owners.username IS NOT null
LIMIT 1
"""

def __init__(self, service: str, author_ownerid: int):
self.query = BadgeCollection.DEFAULT_RAW_QUERY.format(

Check warning on line 156 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L156

Added line #L156 was not covered by tests
service=service,
author_ownerid=author_ownerid,
)

def _badge_tier(self, metric: GamificationMetric, value: float) -> BadgeTier | None:
for item in BADGE_METRIC_TIER_MAPPINGS[metric]:
threshold, tier = item
if value is None:
return None
elif value >= threshold:
return tier
return None

Check warning on line 168 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L162-L168

Added lines #L162 - L168 were not covered by tests

def retrieve(self, organization_name: str, repository_name: str):
start_date = now() - timedelta(days=30)
with connections["default"].cursor() as cursor:
cursor.execute(self.query, [organization_name, repository_name, start_date])
results = cursor.fetchall()
if not results:
return []

Check warning on line 176 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L171-L176

Added lines #L171 - L176 were not covered by tests

patch_coverage_average, change_coverage_count, pr_count = results[0]

Check warning on line 178 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L178

Added line #L178 was not covered by tests

print("actuals", patch_coverage_average, change_coverage_count, pr_count)

Check warning on line 180 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L180

Added line #L180 was not covered by tests

badges = []
patch_coverage_average_tier = self._badge_tier(

Check warning on line 183 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L182-L183

Added lines #L182 - L183 were not covered by tests
GamificationMetric.PATCH_COVERAGE_AVERAGE, patch_coverage_average
)
if patch_coverage_average_tier:
badges.append(

Check warning on line 187 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L186-L187

Added lines #L186 - L187 were not covered by tests
Badge(
GamificationMetric.PATCH_COVERAGE_AVERAGE,
patch_coverage_average_tier,
)
)
change_coverage_count_tier = self._badge_tier(

Check warning on line 193 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L193

Added line #L193 was not covered by tests
GamificationMetric.CHANGE_COVERAGE_COUNT, change_coverage_count
)
if change_coverage_count_tier:
badges.append(

Check warning on line 197 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L196-L197

Added lines #L196 - L197 were not covered by tests
Badge(
GamificationMetric.CHANGE_COVERAGE_COUNT,
change_coverage_count_tier,
)
)
pr_count_tier = self._badge_tier(GamificationMetric.PR_COUNT, pr_count)
if pr_count_tier:
badges.append(Badge(GamificationMetric.PR_COUNT, pr_count_tier))

Check warning on line 205 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L203-L205

Added lines #L203 - L205 were not covered by tests

return badges

Check warning on line 207 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L207

Added line #L207 was not covered by tests


@leaderboard_data_bindable.field("author")
@sync_to_async
def resolve_leaderboard_data_author(leaderboard_data: LeaderboardData, info) -> Owner:
return leaderboard_data.author

Check warning on line 213 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L213

Added line #L213 was not covered by tests


@leaderboard_data_bindable.field("value")
@sync_to_async
def resolve_leaderboard_data_value(leaderboard_data: LeaderboardData, info) -> float:
return leaderboard_data.value

Check warning on line 219 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L219

Added line #L219 was not covered by tests


@leaderboard_bindable.field("name")
@sync_to_async
def resolve_leaderboard_name(leaderboard: Leaderboard, info) -> str:
return leaderboard.name

Check warning on line 225 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L225

Added line #L225 was not covered by tests


@leaderboard_bindable.field("ranking")
@sync_to_async
def resolve_leaderboard_ranking(leaderboard: Leaderboard, info) -> str:
return leaderboard.ranking

Check warning on line 231 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L231

Added line #L231 was not covered by tests


@badge_bindable.field("name")
@sync_to_async
def resolve_badge_name(badge: Badge, info) -> str:
return badge.name

Check warning on line 237 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L237

Added line #L237 was not covered by tests


@badge_bindable.field("tier")
@sync_to_async
def resolve_badge_tier(badge: Badge, info) -> str:
return badge.tier

Check warning on line 243 in graphql_api/types/gamification/gamification.py

View check run for this annotation

Codecov Notifications / codecov/patch

graphql_api/types/gamification/gamification.py#L243

Added line #L243 was not covered by tests
1 change: 1 addition & 0 deletions graphql_api/types/owner/owner.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ type Owner {
): RepositoryConnection! @cost(complexity: 25, multipliers: ["first", "last"])
username: String
yaml: String
badges(organization: String!, repository: String!): [Badge!]
}
Loading
Loading