-
Notifications
You must be signed in to change notification settings - Fork 6
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
""" | ||
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unable to understand the purpose of this |
||
data = super().to_representation(instance) | ||
return data | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
|
@@ -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 = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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')), | ||
], | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
// update this file in your theme directory |
There was a problem hiding this comment.
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 populatingprofile_image_url
field I think you should create a fieldprofile_image_url
in serializer and then writeget_profile_image_url
method to populate it.