Welcome all.
This blog will be about the implementation of the gamification system we discussed in the cEP.
Summary of what we have to achieve:
- Import all the issues and mrs opened by newcomers in coala org on GitHub and GitLab.
- Create levels.
- Iterate through each of the mrs:
- If the mr is merged:
- Get an activity based on the labels on the mr.
- That activity would be assign with some points.
- Update the total score and current level based on the new points earned for the newcomer who opened the mr.
- Add the activity to the newcomer’s activities field.
- Get the issues that mr is closing.
- Get the activity based on the labels on the issue.
- That activity would be assigned with some points.
- Update the total score and current level based on the new points earend for the newcomer who opened the issue.
- Add the activity to the newcomer’s actvities field.
- If the mr is merged:
- If the mr is closed without merge:
- Get an activity “Closed a mr without merge”
- Deduct points from total score and update the current level.
- Create Badge Activities and Badges.
- Iterate through all newcomers’ activities.
- Award them badges based on those activities.
Throughout this blog I will discuss the work that has been done in building gamification app pr.
Designing Django Data Models
So let’s begin with designing Django models for the first part which involves Activity, Level and Newcomer models:
class Newcomer(models.Model):
username = models.CharField(max_length=100, primary_key=True)
score = models.IntegerField(default=0, null=True)
level = models.ForeignKey(Level, on_delete=models.CASCADE,
default=1, null=True)
activities = models.ManyToManyField(Activity)
We can see that the newcomer model has field name level
ForeignKey with Level
model and activities
ManyToMany with Activity model.
As a newcomer can only be at one level at a time but they can perform multiple activities.
Similarly we can design the Level
and Activity
model:
class Level(models.Model):
number = models.IntegerField(primary_key=True)
min_score = models.BigIntegerField()
max_score = models.BigIntegerField()
name = models.TextField()
min_score and max_score is the minimum and maximum score required to be in that particular level.
class Activity(models.Model):
name = models.TextField()
points = models.IntegerField()
# Number of times this activity has been performed by the
# same newcomer
number_of_times = models.IntegerField(default=1, null=True)
As commented on the
number_of_times
field, it’s required in case of the same newcomer perform the same activity multiple times. E.g. If a newcomer performs an activityCreated a Newcomer bug
twice then we will increase the number of times field for that activity with one instead of adding a new activity.
Now, let’s add some methods to the Newcomer
model to add points, set levels and add activities:
A method to find suitable level based on the total score earned:
def find_level_for_score(self, score):
level = Level.objects.get(min_score__lte=score, max_score__gt=score)
return level
A method to update the score and levels:
def update_score_and_level(self, points):
"""
Update score and level based on points.
"""
if points < 0 and self.score < abs(points):
new_score = self.score = 0
else:
self.score += points
new_score = self.score
new_level = self.find_level_for_score(new_score)
if new_level.number > self.level.number:
self.level = new_level
A method to add activities:
def add_activity(self, points, activity_string):
"""
Add activity to the newcomer.
This method checks if the current activity is
already peformed by the user, if yes, then it
increase the 'number_of_times' field with one.
If not then it adds a new activity to the newcomer.
"""
activity, created = Activity.objects.get_or_create(
name=activity_string, points=points)
if created:
activity.save()
self.activities.add(activity)
else:
activity.number_of_times += 1
activity.save()
Lets call all these methods with a single method:
def update_newcomer_data(self, points, activity_string):
self.update_score_and_level(points)
self.add_activity(points, activity_string)
Now, whenever we call the method
update_newcomer_data
with passing two parameterpoints
andactivity
it will update total score, current level and adds that activity for the newcomer who performed the same.
Getting Activities based on Labels
I have created two methods get_issues_activity
and get_mrs_activity
which takes a QuerySet dict containing the ‘name’ as key and ‘name of the label’ as value and return a tuple of points and activity string.
Creating Gamfication Data
Before going further we need to have some initial data in the database like levels
and newcomers
.
I have defined a method create_levels
which initialized the levels object we needed in a list and then we can use bulk_create
method to create all the levels with one query.
Similarly, we can initialize all the necomers objects in a list and then use bulk_create
method to create hundreds of newcomers at once.
E.g. We can get the newcomers_list from the function get_newcomers
and create all the newcomers which will be involved in the gamification app with their initial data by doing the following:
newcomer_objects_list = []
for newcomer in get_newcomers():
newcomer_objects_list.append(
Newcomer(username=newcomer))
Newcomer.objects.bulk_create(newcomer_objects_list)
Updating Gamfication Data
Now the time has come to iterate through the mrs and update the newcomers data:
def update_newcomers_data(mr):
"""
Update total score earned by the
newcomer based on the activities performed,
and the update the current level based on the
total score.
This method first check if the mr is merged or not,
if it's merged, then get the activity and points based
on the labels on mr and update the newcomer data who opened
this mr.
Further it gets all the issues this mr is closing and if its
merged and then get the points and activity based on the labels
on the issue and update the newcomer data who opened that issue.
"""
logger = logging.getLogger(__name__)
if mr.state == 'merged':
labels = mr.labels.values('name')
# Get the newcomer who opened this mr
ncm = Newcomer.objects.get(username=mr.author)
try:
# Get activity and points based on labeles on the mr
points, activity_string = get_merge_request_activity(labels)
# Accept Exception if no activities found
except Exception as ex:
logger.error(ex)
return
# Update newcomer data
ncm.add_points(points, activity_string)
ncm.save()
# Get all the issues numbers this mr is closing
issues = mr.closes_issues.all()
repo = mr.repo
for issue in issues:
# Get issue object from issue model
i = Issue.objects.get(number=issue.number, repo=repo)
i_labels = i.labels.values('name')
# Get newcomer who opened the issue
ncm = Newcomer.objects.get(username=i.author)
# Get activity and points based on the labels on the issue
points, activity_string = get_issue_activity(i_labels)
# Update newcomer data who opened the issue
ncm.add_points(points, activity_string)
ncm.save()
if mr.state == 'closed':
ncm = Newcomer.objects.get(username=mr.author)
points = 5
activity = 'Closed a merge_request without merge'
# Deduct 5 points for closing the merge_request
ncm.deduct_points(points, activity)
ncm.save()
Doing it for Badges
Similar things can be done for badges:
Lets add a badges
field to newcomers model:
badges = models.ManyToMany(Badges)
Create Badge and BadgeActivity Models:
class BadgeActivity(models.Model):
name = models.TextField()
# Number of times a newcomer have to perform this activity
# to get this badge.
number_of_times = models.IntegerField(default=1, null=True)
class Badge(models.Model):
number = models.IntegerField(primary_key=True)
name = models.CharField(max_length=200)
details = models.TextField(null=True)
# Activities a newcomer have to perform to get this badge
b_activities = models.ManyToManyField(BadgeActivity)
Now lets add some methods to Newcomer
model for awarding badges:
def find_badges_for_activity(self, activities):
"""
Find the badge based on the activities peformed by the newcomer.
:param activities: a QuerySet dict containing the 'name'
as key and 'name of the activity' as value
:return: a badge object
"""
activities = [activity['name'] for activity in activities]
badge_objects_list = []
badges = Badge.objects.all()
for badge in badges:
b_activities = badge.b_activities.values('name')
b_activities = [b_activity['name'] for b_activity in b_activities]
match_activity_list = []
for b_activity in b_activities:
if b_activity in activities:
match_activity_list.append(1)
if b_activities.count() == len(match_activity_list):
badge_objects_list.append(badge)
return badge_objects_list
def add_badge(self, activities):
"""
Add badge to newcomer based on the activities performed.
"""
badges = self.find_badges_for_activity(activities)
if badges.count() == 0:
return
for badge in badges:
self.badges.add(badge)
find_badges_for_activity
will find the badge when all of its activities is in the activities
list.
Creating Badges
First we will create some badge_activities and then define the badges with adding suitable activities in each of them.
E.g. see create_badge_activity
, create_badges
and add_activities_to_badges
methods.
Awarding Badges
There is not much left to do for awarding badges, just iterated through all the newcomers and get all the activities done by them and then call the add_badges
method defined in the Newcomer
model with the activities
parameter:
def award_badges(newcomer):
activities = newcomer.activities.values('name')
newcomer.add_badge(activities)
newcomer.save()
That’s it. Thanks for reading:) Will write about testing this gamification app next!