Skip to content

Commit

Permalink
Show badges on profile page and add logic to award them on relevant a…
Browse files Browse the repository at this point in the history
…ctivities (mozilla#3423)

Display badges in the user profile page. The respective badges are displayed if the user's level for that badge is above 1, and the level of the badge is displayed on the bottom left of the badge.

Adds the relevant stat tracking needed to award badges based on the criteria defined in the specification.
  • Loading branch information
harmitgoswami authored Oct 25, 2024
1 parent 68b36c4 commit 3a67813
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 42 deletions.
33 changes: 33 additions & 0 deletions pontoon/base/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

from django import forms
from django.conf import settings
from django.utils import timezone

from pontoon.base import utils
from pontoon.base.models import (
Locale,
PermissionChangelog,
ProjectLocale,
User,
UserProfile,
Expand Down Expand Up @@ -97,11 +99,42 @@ def assign_users_to_groups(self, group_name, users):

group.user_set.clear()

before_count = self.user.badges_promotion_count
now = timezone.now()

if users:
group.user_set.add(*users)

log_group_members(self.user, group, (add_users, remove_users))

after_count = self.user.badges_promotion_count

# TODO:
# This code is the only consumer of the PermissionChangelog
# model, so we should refactor in the future to simplify
# how promotions are retrieved. (see #2195)

# Check if user was demoted from Manager to Translator
# In this case, it doesn't count as a promotion
if group_name == "managers":
removal = PermissionChangelog.objects.filter(
performed_by=self.user,
action_type=PermissionChangelog.ActionType.REMOVED,
created_at__gte=now,
)
if removal:
for item in removal:
if "managers" in item.group.name:
after_count -= 1

# Award Community Builder badge
if (
after_count > before_count
and after_count in settings.BADGES_PROMOTION_THRESHOLDS
):
# TODO: Send a notification to the user
pass


class LocalePermsForm(UserPermissionLogFormMixin, forms.ModelForm):
translators = forms.ModelMultipleChoiceField(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.16 on 2024-10-17 17:51

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("base", "0066_userprofile_community_builder_level_and_more"),
]

operations = [
migrations.RemoveField(
model_name="userprofile",
name="community_builder_level",
),
migrations.RemoveField(
model_name="userprofile",
name="review_master_level",
),
migrations.RemoveField(
model_name="userprofile",
name="translation_champion_level",
),
]
32 changes: 31 additions & 1 deletion pontoon/base/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models import Count, Q
from django.urls import reverse
from django.utils import timezone

Expand Down Expand Up @@ -220,6 +220,33 @@ def has_approved_translations(self):
return self.translation_set.filter(approved=True).exists()


@property
def badges_translation_count(self):
"""Contributions provided by user that count towards their badges."""
return self.actions.filter(
action_type="translation:created",
created_at__gte=settings.BADGES_START_DATE,
).count()


@property
def badges_review_count(self):
"""Translation reviews provided by user that count towards their badges."""
return self.actions.filter(
Q(action_type="translation:approved") | Q(action_type="translation:rejected"),
created_at__gte=settings.BADGES_START_DATE,
).count()


@property
def badges_promotion_count(self):
"""Role promotions performed by user that count towards their badges"""
return self.changed_permissions_log.filter(
action_type="added",
created_at__gte=settings.BADGES_START_DATE,
).count()


@property
def top_contributed_locale(self):
"""Locale the user has made the most contributions to."""
Expand Down Expand Up @@ -445,6 +472,9 @@ def latest_action(self):
User.add_to_class("locale_role", user_locale_role)
User.add_to_class("status", user_status)
User.add_to_class("contributed_translations", contributed_translations)
User.add_to_class("badges_translation_count", badges_translation_count)
User.add_to_class("badges_review_count", badges_review_count)
User.add_to_class("badges_promotion_count", badges_promotion_count)
User.add_to_class("has_approved_translations", has_approved_translations)
User.add_to_class("top_contributed_locale", top_contributed_locale)
User.add_to_class("can_translate", can_translate)
Expand Down
40 changes: 0 additions & 40 deletions pontoon/base/models/user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,46 +37,6 @@ class Themes(models.TextChoices):
default=Themes.DARK,
)

## Badges ##

# Review Master
review_master_level = models.IntegerField(
default=0,
choices=[
(0, "No Badge"),
(1, "Review Master Level 1"), # Review 5 translations
(2, "Review Master Level 2"), # 50 translations
(3, "Review Master Level 3"), # 250 translations
(4, "Review Master Level 4"), # 1000 translations
],
)

# Translation Champion
translation_champion_level = models.IntegerField(
default=0,
choices=[
(0, "No Badge"),
(1, "Translation Champion Level 1"), # Submit 5 translations
(2, "Translation Champion Level 2"), # 50 translations
(3, "Translation Champion Level 3"), # 250 translations
(4, "Translation Champion Level 4"), # 1000 translations
],
)

# Community Builder
community_builder_level = models.IntegerField(
default=0,
choices=[
(0, "No Badge"),
(
1,
"Community Builder Level 1",
), # Successfully promote 1 contributor to a new role
(2, "Community Builder Level 2"), # 2 contributors
(3, "Community Builder Level 3"), # 5 contributors
],
)

# External accounts
chat = models.CharField("Chat username", max_length=255, blank=True, null=True)
github = models.CharField("GitHub username", max_length=255, blank=True, null=True)
Expand Down
29 changes: 29 additions & 0 deletions pontoon/contributors/static/css/profile.css
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,35 @@ h2 {
width: 100%;
}

.achievements .block {
font-size: 0;
}

.achievements .badge-wrapper {
position: relative;
display: inline-block;
margin: 10px 7px 0 0;
}

.achievements .badge-wrapper:last-child {
margin-right: 0;
}

.achievements img.badge {
border: 2px solid var(--main-border-1);
border-radius: 50%;
}

.achievements .badge-level {
position: absolute;
bottom: 5px;
left: 2px;
background: var(--icon-background-1);
padding: 2px 4px;
border-radius: 4px;
font-size: 12px;
}

h4 {
font-size: 14px;
}
Expand Down
11 changes: 11 additions & 0 deletions pontoon/contributors/static/img/community_builder_badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions pontoon/contributors/static/img/review_master_badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions pontoon/contributors/static/img/translation_champion_badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions pontoon/contributors/templates/contributors/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

{% set is_my_profile = (user.is_authenticated and user.email == contributor.email) %}
{% set profile = contributor.profile %}
{% set has_badges = badges.review_master_badge.level or badges.translation_champion_badge.level or badges.community_builder_badge.level %}
{% set translator_for_locales = contributor.translator_for_locales %}
{% set manager_for_locales = contributor.manager_for_locales %}
{% set user_is_translator = user.translated_locales %}
Expand Down Expand Up @@ -121,6 +122,25 @@ <h2 class="display-name">{{ contributor.first_name }}</h2>
<hr>
{% endif %}

{% if has_badges %}
<div class="achievements">
<div class="block">
<h4 class="subtitle">Achievements</h4>
{% for path, badge in badges.items() %}
{% if badge.level %}
<div class="badge-wrapper">
<img class="badge" src="{{ static('img/' ~ path ~ '.svg') }}" title="{{ badge.name }}: Level {{ badge.level }}" />
{% if badge.level > 1 %}
<div class="badge-level">{{ badge.level }}x</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
<hr>
{% endif %}

<div class="dates">
<div class="block">
<h4 class="subtitle">Latest activity</h4>
Expand Down
Loading

0 comments on commit 3a67813

Please sign in to comment.