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

codeWOF Gamification with changes #196

Merged
merged 101 commits into from
Apr 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
a204c96
Implemented points system with deductions for incomplete attempts
MareePalmer Jul 3, 2019
bb50db9
Fixed unit tests for profile.
MareePalmer Jul 16, 2019
d338cb2
Got badges working on CodeWOF website. Moved ProfileView functions to…
MareePalmer Jul 23, 2019
ef4c80a
Added badges for consecutive days with attempts. Implementation begun…
MareePalmer Jul 24, 2019
722225f
Completed consecutive day badge calculations in async attempt request.
MareePalmer Aug 2, 2019
e031a04
Added test data generator for test classes in codewof system
MareePalmer Aug 7, 2019
dd6ebb1
Merge branch 'develop' into issue/gamification
MareePalmer Sep 4, 2019
5225b60
Ensured points system worked in merged app. Added points in dashbar b…
MareePalmer Sep 17, 2019
2be31d2
Added custom backdate command. Fixed points adding per question - now…
MareePalmer Sep 17, 2019
4011b33
Added custom badge card html - not quite functional yet
MareePalmer Sep 17, 2019
1b4bd13
Added code for toasts when badges earned (toasts still don't show - b…
MareePalmer Sep 18, 2019
7e093a0
Attempted to use UserFactory to generate test users, unsuccessful. Ad…
MareePalmer Sep 19, 2019
06c93ed
Merge branch 'develop' into issue/gamification
MareePalmer Sep 19, 2019
1d172bb
Added badge point calculations
MareePalmer Sep 19, 2019
7e83a04
Toasts now showing. Custom text based on badge earned yet to be imple…
MareePalmer Sep 19, 2019
35a6839
Added feature files to detail gamification tests.
Sep 19, 2019
08d7e8c
Changed sampledata creation badge to be tier 0 (no points). Added mor…
Sep 19, 2019
471e43a
Removed activity graph from dashboard. Fixed toasts to show custom ba…
MareePalmer Sep 20, 2019
891aad1
Updated feature files
Sep 20, 2019
3505543
Merge https://github.com/uccser/codewof into develop
Sep 20, 2019
05a27d8
Altered file style and updated docstrings to comply with style rules.
MareePalmer Sep 23, 2019
244cf7d
Merge branch 'develop' of https://github.com/mpa588/codewof into develop
MareePalmer Sep 23, 2019
ad62562
Changed px to rem for activity graph css
MareePalmer Sep 23, 2019
452e77b
Renamed variables and rewrote documentation for function to get numbe…
MareePalmer Sep 24, 2019
c95d012
Only shows earned badges in the dashboard. Adjusted badge padding
MareePalmer Oct 22, 2019
dfe7eaf
Added point notifications. Implemented parent field in model class fo…
MareePalmer Oct 22, 2019
44d63fb
fixed style in sampledata
MareePalmer Oct 23, 2019
7643db8
Resolve merge conflicts.
courtneycb Mar 20, 2020
074f373
Correct rem unit conversion.
courtneycb Mar 20, 2020
1eba463
Fix badge wording alignment.
courtneycb Mar 25, 2020
4d69fc1
Add achievements summary page.
courtneycb Mar 27, 2020
e36925b
Merge branch 'develop' into gamification
courtneycb Mar 30, 2020
04a8213
Review changes.
courtneycb Mar 30, 2020
7abc06a
Add link to achievements page to badges on dashboard.
courtneycb Mar 30, 2020
8be4d74
Tidy up. Trying to get toasts to be sticky.
courtneycb Mar 30, 2020
4a2b6ed
add backdate as manage.py command
eAlasdair Mar 31, 2020
832bc67
Remove unused functions from backdate command.
courtneycb Mar 31, 2020
baf64ee
Tidy up print statements in command.
courtneycb Mar 31, 2020
fba297a
Remove account creation badge.
courtneycb Mar 31, 2020
93b9e94
Remove magic numbers.
courtneycb Mar 31, 2020
bafd65c
Hide user names during backdate for privacy
eAlasdair Apr 1, 2020
717c2af
fix style
eAlasdair Apr 1, 2020
f071773
collate backdate code and remove magic numbers
eAlasdair Apr 1, 2020
a6f5fd3
Add badges to admin.
courtneycb Apr 1, 2020
fb5e644
change name of user2 acc.
courtneycb Apr 1, 2020
fe87e80
Remove user2 from sampledata.
courtneycb Apr 1, 2020
2eef186
add more detail to admin views
eAlasdair Apr 2, 2020
d3a3458
style
eAlasdair Apr 2, 2020
2c77e23
ROUGH. Tests working. Had to revert pytest. Essentially removes codew…
courtneycb Apr 2, 2020
cedd108
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 2, 2020
7a22f36
style
courtneycb Apr 2, 2020
eef1750
Add back in created account badge.
courtneycb Apr 2, 2020
f3a553a
Fix up some tests.
courtneycb Apr 2, 2020
9c36fe4
Style.
courtneycb Apr 2, 2020
29f7a8b
make separate load_badges command and use it wherever load_questions …
eAlasdair Apr 3, 2020
9692e20
style
eAlasdair Apr 3, 2020
44f8e7c
add backdate command to deployment update content code
eAlasdair Apr 3, 2020
190e89b
add attempted question inline back into admin profile view
eAlasdair Apr 7, 2020
ada428b
Fix testing user login.
courtneycb Apr 7, 2020
d00c6dc
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 7, 2020
1bc341d
Naive fix for sampledate not awarding points
eAlasdair Apr 7, 2020
3812009
fix naive datetimes in sampledata
eAlasdair Apr 7, 2020
c0b81b4
remove attempted questions from profile view
eAlasdair Apr 7, 2020
e8d8134
Basic tests passing.
courtneycb Apr 7, 2020
38fb2b0
Add basic user test.
courtneycb Apr 7, 2020
d6ead4e
Add log of earned badges to admin view
eAlasdair Apr 8, 2020
4ed9e31
style
eAlasdair Apr 8, 2020
6dc9053
add full stop for style
eAlasdair Apr 8, 2020
322bb13
redo load_badges command and fix incorrect asset reference
eAlasdair Apr 8, 2020
5bb68a3
Fix up programming model tests.
courtneycb Apr 8, 2020
d5edad1
Add codewof utils tests.
courtneycb Apr 8, 2020
ec2db4b
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 8, 2020
72b7a2c
Remove redundant codewof folder.
courtneycb Apr 8, 2020
bdfe098
Shift code that should be in models tests.
courtneycb Apr 8, 2020
aaa0e52
Add base question model tests.
courtneycb Apr 8, 2020
20bbddc
Delete old views file. Inheritance manager sucks.
courtneycb Apr 9, 2020
8586d64
Remove redundant index view.
courtneycb Apr 9, 2020
e530084
Fix question model tests.
courtneycb Apr 9, 2020
2746b6c
migration for meta change
eAlasdair Apr 9, 2020
6ae564c
Add QuestionType model tests.
courtneycb Apr 9, 2020
fe7944a
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 9, 2020
b6869b5
Remove old code.
courtneycb Apr 9, 2020
31cf22a
Latest.
courtneycb Apr 9, 2020
4b28bd4
Add some user model tests.
courtneycb Apr 10, 2020
9573670
Fix days consecutively answered function and test
eAlasdair Apr 10, 2020
d43f9bf
Tidy up.
courtneycb Apr 10, 2020
62158b7
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 10, 2020
57e3130
minor clarity fixes for utils
eAlasdair Apr 10, 2020
2ab116a
Commit before pull.
courtneycb Apr 10, 2020
15cd2bd
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 10, 2020
33a3419
Add more utils tests.
courtneycb Apr 10, 2020
bcef44a
Update check_badge_conditions test.
courtneycb Apr 14, 2020
f822d50
Pushing latest.
courtneycb Apr 14, 2020
f255701
fix test
eAlasdair Apr 14, 2020
1b85b8e
Add some tests for backdate function
courtneycb Apr 14, 2020
d90fb9f
All da style thaaaaangs.
courtneycb Apr 14, 2020
d77fce2
add tests for earning points under certain conditions
eAlasdair Apr 14, 2020
1f7bd87
Merge branch 'gamification' of https://github.com/uccser/codewof into…
eAlasdair Apr 14, 2020
fb08dc1
fix
eAlasdair Apr 14, 2020
0a82dad
Fix feature file scenario.
courtneycb Apr 14, 2020
54451db
Merge branch 'gamification' of https://github.com/uccser/codewof into…
courtneycb Apr 14, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions codewof/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Init file for codeWOF."""
5 changes: 4 additions & 1 deletion codewof/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import environ
from utils.get_upload_filepath import get_upload_path_for_date


# codewof/codewof/config/settings/base.py - 3 = codewof/codewof/
ROOT_DIR = environ.Path(__file__) - 3

Expand Down Expand Up @@ -316,6 +315,10 @@
'ignore_params': 'no'
}

ACTIVE_URL_CACHE = True
ACTIVE_URL_CACHE_TIMEOUT = 60 * 60 * 24 # 1 day
ACTIVE_URL_CACHE_PREFIX = 'django_activeurl'

# django-rest-framework
# ------------------------------------------------------------------------------
REST_FRAMEWORK = {
Expand Down
17 changes: 12 additions & 5 deletions codewof/general/management/commands/sampledata.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def handle(self, *args, **options):
primary=True,
verified=True
)
print('Admin created.')
print('Admin created.\n')

# Create user account
user = User.objects.create_user(
Expand All @@ -67,18 +67,25 @@ def handle(self, *args, **options):
primary=True,
verified=True
)

UserFactory.create_batch(size=100)
print('Users created.')
print('Users created.\n')

# Codewof
management.call_command('load_questions')
print('Programming questions loaded.')
print('Programming questions loaded.\n')

management.call_command('load_badges')
print('Achievement badges loaded.\n')

# Research
StudyFactory.create_batch(size=5)
StudyGroupFactory.create_batch(size=15)
print('Research studies loaded.')
print('Research studies loaded.\n')

# Attempts
AttemptFactory.create_batch(size=50)
print('Attempts loaded.')
print('Attempts loaded.\n')

# Award points and badges
management.call_command('backdate_points_and_badges')
39 changes: 38 additions & 1 deletion codewof/programming/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
QuestionTypeProgram,
QuestionTypeFunction,
QuestionTypeParsons,
QuestionTypeDebugging
QuestionTypeDebugging,
Profile,
Badge,
Earned,
)

User = get_user_model()
Expand All @@ -23,6 +26,37 @@ class TestCaseAttemptInline(admin.TabularInline):
can_delete = False


class EarnedInline(admin.TabularInline):
"""Configuration to show earned badges inline within profile admin."""

model = Earned
extra = 1


class ProfileAdmin(admin.ModelAdmin):
"""Configuration for displaying profiles in admin."""

list_display = ('user', 'points', 'goal')
ordering = ('user', )
inlines = (EarnedInline, )


class BadgeAdmin(admin.ModelAdmin):
"""Configuration for displaying badges in admin."""

list_display = ('id_name', 'display_name', 'badge_tier')
list_filter = ['badge_tier']
ordering = ('id_name', )


class EarnedAdmin(admin.ModelAdmin):
"""Configuration for displaying earned badges in admin."""

list_display = ('date', 'badge', 'profile')
list_filter = ['badge']
ordering = ('-date', )


class AttemptAdmin(admin.ModelAdmin):
"""Configuration for displaying attempts in admin."""

Expand All @@ -43,3 +77,6 @@ class Media:
admin.site.register(QuestionTypeFunction)
admin.site.register(QuestionTypeParsons)
admin.site.register(QuestionTypeDebugging)
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Badge, BadgeAdmin)
admin.site.register(Earned, EarnedAdmin)
249 changes: 249 additions & 0 deletions codewof/programming/codewof_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
"""Utility functions for codeWOF system. Involves points, badges, and backdating points and badges per user."""

import datetime
import json
import logging
from dateutil.relativedelta import relativedelta

from programming.models import (
Profile,
Question,
Attempt,
Badge,
Earned,
)
from django.http import JsonResponse
# from django.conf import settings

# time_zone = settings.TIME_ZONE

logger = logging.getLogger(__name__)
del logging

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'incremental': True,
'root': {
'level': 'DEBUG',
},
}

# Number of points awarded for achieving each goal
POINTS_BADGE = 10
POINTS_SOLUTION = 10
POINTS_BONUS = 2


def add_points(question, profile, attempt):
"""
Add appropriate number of points (if any) to user's profile after a question is answered.

Adds points to a user's profile for when the user answers a question correctly for the first time. If the user
answers the question correctly the first time they answer, the user gains bonus points.

Subsequent correct answers should not award any points.
"""
num_attempts = Attempt.objects.filter(question=question, profile=profile)
is_first_correct = len(Attempt.objects.filter(question=question, profile=profile, passed_tests=True)) == 1
logger.warning(num_attempts)
logger.warning(is_first_correct)
points_to_add = 0

# check if first passed
if attempt.passed_tests and is_first_correct:
points_to_add += POINTS_SOLUTION
if len(num_attempts) == 1:
# correct first try
points_to_add += POINTS_BONUS

profile.points += points_to_add
profile.full_clean()
profile.save()
return profile.points


def save_goal_choice(request):
"""Update user's goal choice in database."""
request_json = json.loads(request.body.decode('utf-8'))
if request.user.is_authenticated:
user = request.user
profile = user.profile

goal_choice = request_json['goal_choice']
profile.goal = int(goal_choice)
profile.full_clean()
profile.save()

return JsonResponse({})


def get_days_consecutively_answered(user):
"""
Get the number of consecutive days with questions attempted.

Gets all datetimes of attempts for the given user's profile, and checks for the longest continuous "streak" of
days where attempts were made. Returns an integer of the longest attempt "streak".
"""
# get datetimes from attempts in date form)
attempts = Attempt.objects.filter(profile=user.profile).datetimes('datetime', 'day', 'DESC')

if len(attempts) <= 0:
return 0

# first attempt is the start of the first streak
streak = 1
highest_streak = 0
expected_date = attempts[0].date() - datetime.timedelta(days=1)

for attempt in attempts[1:]:
if attempt.date() == expected_date:
# continue the streak
streak += 1
else:
# streak has ended
if streak > highest_streak:
highest_streak = streak
streak = 1
# compare the next item to yesterday
expected_date = attempt.date() - datetime.timedelta(days=1)

if streak > highest_streak:
highest_streak = streak

return highest_streak


def get_questions_answered_in_past_month(user):
"""Get the number questions successfully answered in the past month."""
today = datetime.datetime.now().replace(tzinfo=None) + relativedelta(days=1)
last_month = today - relativedelta(months=1)
solved = Attempt.objects.filter(profile=user.profile, datetime__gte=last_month.date(), passed_tests=True)
return len(solved)


def check_badge_conditions(user):
"""
Check if the user has earned new badges for their profile.

Checks if the user has received each available badge. If not, check if the user has earned these badges. Badges
available to be checked for are profile creation, number of attempts made, number of questions answered, and
number of days with consecutive attempts.

A badge will not be removed if the user had earned it before but now doesn't meet the conditions
"""
earned_badges = user.profile.earned_badges.all()
new_badge_names = ""
new_badge_objects = []
# account creation badge
try:
creation_badge = Badge.objects.get(id_name="create-account")
if creation_badge not in earned_badges:
# create a new account creation
new_achievement = Earned(profile=user.profile, badge=creation_badge)
new_achievement.full_clean()
new_achievement.save()
new_badge_names = new_badge_names + "- " + creation_badge.display_name + "\n"
new_badge_objects.append(creation_badge)
except Badge.DoesNotExist:
logger.warning("No such badge: create-account")
pass

# check questions solved badges
try:
question_badges = Badge.objects.filter(id_name__contains="questions-solved")
solved = Attempt.objects.filter(profile=user.profile, passed_tests=True)
for question_badge in question_badges:
if question_badge not in earned_badges:
num_questions = int(question_badge.id_name.split("-")[2])
if len(solved) >= num_questions:
new_achievement = Earned(profile=user.profile, badge=question_badge)
new_achievement.full_clean()
new_achievement.save()
new_badge_names = new_badge_names + "- " + question_badge.display_name + "\n"
new_badge_objects.append(question_badge)
except Badge.DoesNotExist:
logger.warning("No such badges: questions-solved")
pass

# checked questions attempted badges
try:
attempt_badges = Badge.objects.filter(id_name__contains="attempts-made")
attempted = Attempt.objects.filter(profile=user.profile)
for attempt_badge in attempt_badges:
if attempt_badge not in earned_badges:
num_questions = int(attempt_badge.id_name.split("-")[2])
if len(attempted) >= num_questions:
new_achievement = Earned(profile=user.profile, badge=attempt_badge)
new_achievement.full_clean()
new_achievement.save()
new_badge_names = new_badge_names + "- " + attempt_badge.display_name + "\n"
new_badge_objects.append(attempt_badge)
except Badge.DoesNotExist:
logger.warning("No such badges: attempts-made")
pass

# consecutive days logged in badges
num_consec_days = get_days_consecutively_answered(user)
consec_badges = Badge.objects.filter(id_name__contains="consecutive-days")
for consec_badge in consec_badges:
if consec_badge not in earned_badges:
n_days = int(consec_badge.id_name.split("-")[2])
if n_days <= num_consec_days:
new_achievement = Earned(profile=user.profile, badge=consec_badge)
new_achievement.full_clean()
new_achievement.save()
new_badge_names = new_badge_names + "- " + consec_badge.display_name + "\n"
new_badge_objects.append(consec_badge)

calculate_badge_points(user, new_badge_objects)
return new_badge_names


def calculate_badge_points(user, badges):
"""Calculate points earned by the user for new badges earned by multiplying the badge tier by 10."""
for badge in badges:
points = badge.badge_tier * POINTS_BADGE
user.profile.points += points
user.full_clean()
user.save()


def backdate_points_and_badges():
"""Perform backdate of all points and badges for each profile in the system."""
profiles = Profile.objects.all()
num_profiles = len(profiles)
for i in range(num_profiles):
print("Backdating users: " + str(i + 1) + "/" + str(num_profiles), end="\r")
profile = profiles[i]
profile = backdate_badges(profile)
profile = backdate_points(profile)
# save profile when update is completed
profile.full_clean()
profile.save()
print("\nBackdate complete.")


def backdate_points(profile):
"""Re-calculate points for the user profile."""
questions = Question.objects.all()
profile.points = 0
for question in questions:
has_passed = len(Attempt.objects.filter(profile=profile, question=question, passed_tests=True)) > 0
user_attempts = Attempt.objects.filter(profile=profile, question=question)
first_passed = False
if len(user_attempts) > 0:
first_passed = user_attempts[0].passed_tests
if has_passed:
profile.points += POINTS_SOLUTION
if first_passed:
profile.points += POINTS_BONUS
for badge in profile.earned_badges.all():
profile.points += POINTS_BADGE * badge.badge_tier
return profile


def backdate_badges(profile):
"""Re-check the profile for badges earned."""
check_badge_conditions(profile.user)
return profile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Module for the custom Django backdate_points_and_badges command."""

from django.core.management.base import BaseCommand
from programming.codewof_utils import backdate_points_and_badges


class Command(BaseCommand):
"""Required command class for the custom Django backdate command."""

help = 'Loads questions into the database'

def handle(self, *args, **options):
"""Automatically called when the backdate command is given."""
backdate_points_and_badges()