Skip to content

Commit

Permalink
update: add leaderboard apis
Browse files Browse the repository at this point in the history
  • Loading branch information
Ali Salman authored and Ali Salman committed Nov 20, 2023
1 parent 4d4e482 commit a79587e
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 4 deletions.
4 changes: 3 additions & 1 deletion lms/djangoapps/badges/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
BadgeAssertion,
BadgeClass,
CourseCompleteImageConfiguration,
CourseEventBadgesConfiguration
CourseEventBadgesConfiguration,
LeaderboardConfiguration
)

admin.site.register(CourseCompleteImageConfiguration)
admin.site.register(BadgeClass)
admin.site.register(BadgeAssertion)
# Use the standard Configuration Model Admin handler for this model.
admin.site.register(CourseEventBadgesConfiguration, ConfigurationModelAdmin)
admin.site.register(LeaderboardConfiguration, ConfigurationModelAdmin)
33 changes: 33 additions & 0 deletions lms/djangoapps/badges/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

from rest_framework import serializers

from django.contrib.auth import get_user_model
from lms.djangoapps.badges.models import BadgeAssertion, BadgeClass
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user

User = get_user_model()

class BadgeClassSerializer(serializers.ModelSerializer):
"""
Expand All @@ -28,3 +31,33 @@ class BadgeAssertionSerializer(serializers.ModelSerializer):
class Meta:
model = BadgeAssertion
fields = ('badge_class', 'image_url', 'assertion_url', 'created')


class BadgeUserSerializer(serializers.ModelSerializer):
"""
Serializer for the BadgeAssertion model.
"""
name = serializers.CharField(source='profile.name')

class Meta:
model = User
fields = ('username', 'name')

def to_representation(self, instance):
data = super().to_representation(instance)
data['profile_image_url'] = get_profile_image_urls_for_user(instance)['medium']
return data


class UserLeaderboardSerializer(serializers.Serializer):
user = BadgeUserSerializer()
badge_count = serializers.IntegerField()
course_badge_count = serializers.IntegerField()
event_badge_count = serializers.IntegerField()
score = serializers.IntegerField()
badges = BadgeAssertionSerializer(many=True)

def to_representation(self, instance):
data = super().to_representation(instance)
return data

4 changes: 3 additions & 1 deletion lms/djangoapps/badges/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from django.conf import settings
from django.urls import re_path

from .views import UserBadgeAssertions
from .views import UserBadgeAssertions, LeaderboardView

urlpatterns = [
re_path('^assertions/user/' + settings.USERNAME_PATTERN + '/$',
UserBadgeAssertions.as_view(), name='user_assertions'),

re_path('leaderboard/', LeaderboardView.as_view(), name='leaderboard')
]
67 changes: 65 additions & 2 deletions lms/djangoapps/badges/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@
from opaque_keys.edx.keys import CourseKey
from rest_framework import generics
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.pagination import PageNumberPagination

from lms.djangoapps.badges.models import BadgeAssertion
from django.db.models import Count, Case, When, Value, IntegerField, Sum
from django.utils.translation import gettext as _
from lms.djangoapps.badges.models import BadgeAssertion, LeaderboardConfiguration
from openedx.core.djangoapps.user_api.permissions import is_field_shared_factory
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user

from .serializers import BadgeAssertionSerializer
from .serializers import BadgeAssertionSerializer, UserLeaderboardSerializer


class InvalidCourseKeyError(APIException):
Expand Down Expand Up @@ -137,3 +143,60 @@ def get_queryset(self):
badge_class__issuing_component=self.request.query_params.get('issuing_component', '')
)
return queryset


class LeaderboardView(generics.ListAPIView):
"""
Leaderboard List API View
"""
serializer_class = UserLeaderboardSerializer

def get_queryset(self):
"""
leaderboard queryset
"""
leaderboard_conf = LeaderboardConfiguration.current()

if leaderboard_conf and leaderboard_conf.enabled:
course_badge_score = leaderboard_conf.course_badge_score
event_badge_score = leaderboard_conf.event_badge_score
else:
course_badge_score = LeaderboardConfiguration.COURSE_BADGE_SCORE
event_badge_score = LeaderboardConfiguration.EVENT_BADGE_SCORE

leaderboard_data = (
BadgeAssertion.objects
.values('user__username', 'badge_class__issuing_component')
.annotate(
points=Case(
When(badge_class__issuing_component='', then=Value(course_badge_score)),
When(badge_class__issuing_component='openedx__course', then=Value(event_badge_score)),
default=Value(0),
output_field=IntegerField()
)
).values('user__username')
.annotate(score=Sum('points'))
.order_by('-score')
)

formatted_data = []
for entry in leaderboard_data:
badges = (
BadgeAssertion.objects
.filter(user__username=entry['user__username'])
.order_by('-created')
)
badge_count = badges.count()
event_badge_count = badges.filter(badge_class__issuing_component='openedx__course').count()
course_badge_count = badge_count - event_badge_count

formatted_data.append({
'user': badges[0].user,
'badge_count':badge_count,
'event_badge_count': event_badge_count,
'course_badge_count': course_badge_count,
'score': entry['score'],
'badges': list(badges),
})

return formatted_data
27 changes: 27 additions & 0 deletions lms/djangoapps/badges/migrations/0005_leaderboardconfiguration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.21 on 2023-11-19 18:58

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('badges', '0004_badgeclass_badgr_server_slug'),
]

operations = [
migrations.CreateModel(
name='LeaderboardConfiguration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('course_badge_score', models.IntegerField(default=50, help_text='Set the score for a course-completion badge')),
('event_badge_score', models.IntegerField(default=50, help_text='Set the score for the event badge i.e program badge')),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
],
),
]
20 changes: 20 additions & 0 deletions lms/djangoapps/badges/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,23 @@ def clean_fields(self, exclude=tuple()):

class Meta:
app_label = "badges"


class LeaderboardConfiguration(ConfigurationModel):
"""
Model for configuring scores for courses and events badges
"""
COURSE_BADGE_SCORE = 50
EVENT_BADGE_SCORE = 50

course_badge_score = models.IntegerField(
help_text='Set the score for a course-completion badge',
default=COURSE_BADGE_SCORE,
)
event_badge_score = models.IntegerField(
help_text='Set the score for the event badge i.e program badge',
default=EVENT_BADGE_SCORE,
)

class Meta:
app_label = "badges"
1 change: 1 addition & 0 deletions lms/static/js/dashboard/badges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// update this file in your theme directory

0 comments on commit a79587e

Please sign in to comment.