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

update: add leaderboard apis #452

Merged
merged 4 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think its good idea to override to_representation for populating profile_image_url field I think you should create a field profile_image_url in serializer and then write get_profile_image_url method to populate it.

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):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unable to understand the purpose of this to_representation override. Could you please explain?

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 = (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems very compute intensive query it might not work with large dataset. can we pre compute these on badge generation events to avoid this query scanning complete badge table?

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
Loading