I'm building a leaderboard view for a REST API I'm designing in DRF. I need a bit of help in reducing an inefficiency.
views.py
class LeaderboardAPIView(ListAPIView):
serializer_class = UserSerializerLeaderboard
permission_classes = (IsAuthenticated,)
def get_queryset(self):
queryset = User.objects.all()
queryset = list(queryset)
queryset.sort(key=operator.attrgetter("total_karma"), reverse=True)
queryset = queryset[:10]
return queryset
serializers.py
class UserSerializerLeaderboard(serializers.ModelSerializer):
score = serializers.SerializerMethodField(read_only=True)
place = serializers.SerializerMethodField(read_only=True)
def get_score(self, obj):
return obj.total_karma
def get_place(self, obj):
return "1"
class Meta:
model = User
fields = ("score", "place")
models.py
@property
def total_karma(self):
return self.total_post_score() + self.total_comment_score()
def total_post_score(self):
relevant_votes = PostVote.objects.filter(post__user=self)
total = 0
for vote in relevant_votes:
total += vote.vote
return total
def total_comment_score(self):
relevant_votes = CommentVote.objects.filter(comment__user=self)
total = 0
for vote in relevant_votes:
total += vote.vote
return total
...
class PostVote(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True)
vote = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ("user", "post")
def __str__(self):
return str(self.id) + " " + str(self.vote)
...
class CommentVote(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
comment = models.ForeignKey(Comment, on_delete=models.SET_NULL, null=True)
vote = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ("user", "comment")
def __str__(self):
return str(self.id) + " " + str(self.vote)
The get_place method currently returns a placeholder of 1. I'd like it to return the actual place the user is in, sorted by score. Now, I know I'm already calculating this in the queryset, so I don't want to repeat this inefficiently. How can I send the place of the user to the serializer directly, rather than repeating the calculation in the method?
You can use a window function to annotate each row in your queryset with it's position/row number
Change the total_karma
property and the methods used to annotations in your queryset so that you can order by that field and access the calculated result from the returned objects without having to calculate it again
from django.db.models import Window, F, Sum
from django.db.models.functions import RowNumber
class LeaderboardAPIView(ListAPIView):
serializer_class = UserSerializerLeaderboard
permission_classes = (IsAuthenticated,)
def get_queryset(self):
queryset = User.objects.all()
queryset = queryset.annotate(
post_score=Sum('postvote__vote'),
comment_score=Sum('commentvote__vote')
)
queryset = queryset.annotate(
total_karma=F('post_score') + F('comment_score')
)
queryset = queryset.order_by(
'-total_karma'
).annotate(row_num=Window(
expression=RowNumber(),
order_by=F('total_karma').desc()
))[:10]
return queryset
Serializer
class UserSerializerLeaderboard(serializers.ModelSerializer):
score = serializers.SerializerMethodField(read_only=True)
place = serializers.SerializerMethodField(read_only=True)
def get_score(self, obj):
return obj.total_karma
def get_place(self, obj):
return obj.row_num
class Meta:
model = User
fields = ("score", "place")
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.