diff --git a/pontoon/insights/chs.py b/pontoon/insights/chs.py index 0b19585abe..a3841d84d5 100644 --- a/pontoon/insights/chs.py +++ b/pontoon/insights/chs.py @@ -90,9 +90,11 @@ def get_key_projects_enabled_by_locale( return {pl_count["locale_id"]: pl_count["count"] for pl_count in pl_counts} -def get_contributor_metrics_by_locale(locales, end_date: datetime) -> dict[int, dict]: +def get_contributor_classification_by_locale( + locales, end_date: datetime +) -> dict[int, dict]: """ - Per-locale active-contributor counts over the 12-month window ending at end_date. + Per-locale active-contributor lists over the 12-month window ending at end_date. """ start_date = end_date - relativedelta(months=13) @@ -149,11 +151,11 @@ def get_contributor_metrics_by_locale(locales, end_date: datetime) -> dict[int, locale_contributors = { locale.pk: { - "active_managers": 0, - "active_translators": 0, - "active_contributors": 0, - "all_contributors": 0, - "new_signups": 0, + "active_managers": set(), + "active_translators": set(), + "active_contributors": set(), + "all_contributors": set(), + "new_signups": set(), } for locale in locales } @@ -173,23 +175,37 @@ def get_contributor_metrics_by_locale(locales, end_date: datetime) -> dict[int, if user_id in managers[locale_id]: if action_count + approved > MANAGER_STRING_THRESHOLD: - locale_contributors[locale_id]["active_managers"] += 1 + locale_contributors[locale_id]["active_managers"].add(user_id) elif user_id in translators[locale_id]: if action_count + approved > TRANSLATOR_STRING_THRESHOLD: - locale_contributors[locale_id]["active_translators"] += 1 + locale_contributors[locale_id]["active_translators"].add(user_id) else: if is_superuser: continue if approved >= ACTIVE_CONTRIBUTOR_STRING_THRESHOLD: - locale_contributors[locale_id]["active_contributors"] += 1 + locale_contributors[locale_id]["active_contributors"].add(user_id) if total >= ALL_CONTRIBUTOR_STRING_THRESHOLD: - locale_contributors[locale_id]["all_contributors"] += 1 + locale_contributors[locale_id]["all_contributors"].add(user_id) if approved >= NEW_SIGNUP_STRING_THRESHOLD and joined >= start_date: - locale_contributors[locale_id]["new_signups"] += 1 + locale_contributors[locale_id]["new_signups"].add(user_id) return locale_contributors +def get_contributor_metrics_by_locale(locales, end_date: datetime) -> dict[int, dict]: + """ + Per-locale active-contributor counts over the 12-month window ending at end_date. + """ + locale_contributors = get_contributor_classification_by_locale(locales, end_date) + + locale_contributor_counts = { + loc_id: {metric: len(contributors) for metric, contributors in counts.items()} + for loc_id, counts in locale_contributors.items() + } + + return locale_contributor_counts + + def scaled_points(count, points) -> float: """Award full points for 2+ people, half for exactly 1, none otherwise.""" if count >= 2: diff --git a/pontoon/insights/static/css/insights.css b/pontoon/insights/static/css/insights.css index 3d4ed2acf4..36af2f0eec 100644 --- a/pontoon/insights/static/css/insights.css +++ b/pontoon/insights/static/css/insights.css @@ -111,11 +111,58 @@ .info { align-self: center; + position: relative; &.met { color: var(--status-translated); font-weight: bold; } + + .metric-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + z-index: 20; + min-width: 140px; + margin-bottom: 10px; + padding: 10px; + background: var(--tooltip-background); + border-radius: 10px; + color: var(--tooltip-color); + font-weight: normal; + text-align: left; + + &:after { + content: ''; + position: absolute; + bottom: -20px; + left: 50%; + transform: translateX(-50%); + border: 10px solid; + border-color: var(--tooltip-background) transparent transparent + transparent; + clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%); + } + + ul { + margin: 0; + padding: 0; + list-style: none; + } + + li { + list-style: none; + } + + a { + display: flex; + align-items: center; + gap: 6px; + color: var(--tooltip-color); + white-space: nowrap; + } + } } &.score-view { diff --git a/pontoon/insights/static/js/insights.js b/pontoon/insights/static/js/insights.js index ed14987889..d95e409bc1 100644 --- a/pontoon/insights/static/js/insights.js +++ b/pontoon/insights/static/js/insights.js @@ -55,6 +55,61 @@ function renderCommunityHealthPanel() { }); } +let contributorTooltipTimer = null; +let activeContributorInfo = null; + +$('body') + .on('mouseenter', '.community-health-table .info[data-metric]', function () { + const info = $(this); + activeContributorInfo = this; + + contributorTooltipTimer = setTimeout(function () { + $.ajax({ + url: '/insights/ajax/locale-contributors/', + global: false, + data: { + locale: info.data('locale'), + metric: info.data('metric'), + }, + success(response) { + if (activeContributorInfo !== info[0] || !response.users) { + return; + } + + const list = $('