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
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_timesfield, it’s required in case of the same newcomer perform the same activity multiple times. E.g. If a newcomer performs an activity
Created a Newcomer bugtwice 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_datawith passing two parameter
activityit 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_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
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
First we will create some badge_activities and then define the badges with adding suitable activities in each of them.
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
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!