diff --git a/codewof/__init__.py b/codewof/__init__.py
new file mode 100644
index 000000000..05a5d4d7a
--- /dev/null
+++ b/codewof/__init__.py
@@ -0,0 +1 @@
+"""Init file for codeWOF."""
diff --git a/codewof/config/settings/base.py b/codewof/config/settings/base.py
index b95776d27..0c5551e0b 100644
--- a/codewof/config/settings/base.py
+++ b/codewof/config/settings/base.py
@@ -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
@@ -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 = {
diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py
index 99398924c..f70f10681 100644
--- a/codewof/general/management/commands/sampledata.py
+++ b/codewof/general/management/commands/sampledata.py
@@ -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(
@@ -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')
diff --git a/codewof/programming/admin.py b/codewof/programming/admin.py
index 8733f527e..da3560d50 100644
--- a/codewof/programming/admin.py
+++ b/codewof/programming/admin.py
@@ -8,7 +8,10 @@
QuestionTypeProgram,
QuestionTypeFunction,
QuestionTypeParsons,
- QuestionTypeDebugging
+ QuestionTypeDebugging,
+ Profile,
+ Badge,
+ Earned,
)
User = get_user_model()
@@ -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."""
@@ -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)
diff --git a/codewof/programming/codewof_utils.py b/codewof/programming/codewof_utils.py
new file mode 100644
index 000000000..1acfc83d2
--- /dev/null
+++ b/codewof/programming/codewof_utils.py
@@ -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
diff --git a/codewof/programming/management/commands/backdate_points_and_badges.py b/codewof/programming/management/commands/backdate_points_and_badges.py
new file mode 100644
index 000000000..d99e6d058
--- /dev/null
+++ b/codewof/programming/management/commands/backdate_points_and_badges.py
@@ -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()
diff --git a/codewof/programming/management/commands/load_badges.py b/codewof/programming/management/commands/load_badges.py
new file mode 100644
index 000000000..f092479e3
--- /dev/null
+++ b/codewof/programming/management/commands/load_badges.py
@@ -0,0 +1,148 @@
+"""Module for the custom Django load_badges command."""
+
+from django.core.management.base import BaseCommand
+from programming.models import Badge
+
+# TODO: Relocate
+BADGES = [
+ {
+ 'id_name': 'create-account',
+ 'display_name': 'Created an account!',
+ 'description': 'Created your very own account',
+ 'icon_name': 'img/icons/badges/icons8-badge-create-account-48.png',
+ 'badge_tier': 0,
+ 'parent': None
+ },
+ {
+ 'id_name': 'questions-solved-100',
+ 'display_name': 'Solved one hundred questions!',
+ 'description': 'Solved one hundred questions',
+ 'icon_name': 'img/icons/badges/icons8-question-solved-gold-50.png',
+ 'badge_tier': 4,
+ 'parent': None
+ },
+ {
+ 'id_name': 'questions-solved-10',
+ 'display_name': 'Solved ten questions!',
+ 'description': 'Solved ten questions',
+ 'icon_name': 'img/icons/badges/icons8-question-solved-silver-50.png',
+ 'badge_tier': 3,
+ 'parent': 'questions-solved-100'
+ },
+ {
+ 'id_name': 'questions-solved-5',
+ 'display_name': 'Solved five questions!',
+ 'description': 'Solved five questions',
+ 'icon_name': 'img/icons/badges/icons8-question-solved-bronze-50.png',
+ 'badge_tier': 2,
+ 'parent': 'questions-solved-10'
+ },
+ {
+ 'id_name': 'questions-solved-1',
+ 'display_name': 'Solved one question!',
+ 'description': 'Solved your very first question',
+ 'icon_name': 'img/icons/badges/icons8-question-solved-black-50.png',
+ 'badge_tier': 1,
+ 'parent': 'questions-solved-5'
+ },
+ {
+ 'id_name': 'attempts-made-100',
+ 'display_name': 'Made one hundred question attempts!',
+ 'description': 'Attempted one hundred questions',
+ 'icon_name': 'img/icons/badges/icons8-attempt-made-gold-50.png',
+ 'badge_tier': 4,
+ 'parent': None
+ },
+ {
+ 'id_name': 'attempts-made-10',
+ 'display_name': 'Made ten question attempts!',
+ 'description': 'Attempted ten questions',
+ 'icon_name': 'img/icons/badges/icons8-attempt-made-silver-50.png',
+ 'badge_tier': 3,
+ 'parent': 'attempts-made-100'
+ },
+ {
+ 'id_name': 'attempts-made-5',
+ 'display_name': 'Made five question attempts!',
+ 'description': 'Attempted five questions',
+ 'icon_name': 'img/icons/badges/icons8-attempt-made-bronze-50.png',
+ 'badge_tier': 2,
+ 'parent': 'attempts-made-10'
+ },
+ {
+ 'id_name': 'attempts-made-1',
+ 'display_name': 'Made your first question attempt!',
+ 'description': 'Attempted one question',
+ 'icon_name': 'img/icons/badges/icons8-attempt-made-black-50.png',
+ 'badge_tier': 1,
+ 'parent': 'attempts-made-5'
+ },
+ {
+ 'id_name': 'consecutive-days-28',
+ 'display_name': 'Worked on coding every day for four weeks!',
+ 'description': 'Attempted at least one question every day for four weeks',
+ 'icon_name': 'img/icons/badges/icons8-calendar-28-50.png',
+ 'badge_tier': 5,
+ 'parent': None
+ },
+ {
+ 'id_name': 'consecutive-days-21',
+ 'display_name': 'Worked on coding every day for three weeks!',
+ 'description': 'Attempted at least one question every day for three weeks',
+ 'icon_name': 'img/icons/badges/icons8-calendar-21-50.png',
+ 'badge_tier': 4,
+ 'parent': 'consecutive-days-28'
+ },
+ {
+ 'id_name': 'consecutive-days-14',
+ 'display_name': 'Worked on coding every day for two weeks!',
+ 'description': 'Attempted at least one question every day for two weeks',
+ 'icon_name': 'img/icons/badges/icons8-calendar-14-50.png',
+ 'badge_tier': 3,
+ 'parent': 'consecutive-days-21'
+ },
+ {
+ 'id_name': 'consecutive-days-7',
+ 'display_name': 'Worked on coding every day for one week!',
+ 'description': 'Attempted at least one question every day for one week',
+ 'icon_name': 'img/icons/badges/icons8-calendar-7-50.png',
+ 'badge_tier': 2,
+ 'parent': 'consecutive-days-14'
+ },
+ {
+ 'id_name': 'consecutive-days-2',
+ 'display_name': 'Worked on coding for two days in a row!',
+ 'description': 'Attempted at least one question two days in a row',
+ 'icon_name': 'img/icons/badges/icons8-calendar-2-50.png',
+ 'badge_tier': 1,
+ 'parent': 'consecutive-days-7'
+ },
+]
+
+
+class Command(BaseCommand):
+ """Required command class for the custom Django load_badges command.
+
+ Future plan: Create full loader like the load_questions command
+ """
+
+ help = 'Loads badges into the database'
+
+ def handle(self, *args, **options):
+ """Automatically called when the load_badges command is given."""
+ all_badges = {}
+
+ for badge in BADGES:
+ all_badges[badge['id_name']], created = Badge.objects.update_or_create(
+ id_name=badge['id_name'],
+ defaults={
+ 'display_name': badge['display_name'],
+ 'description': badge['description'],
+ 'icon_name': badge['icon_name'],
+ 'badge_tier': badge['badge_tier'],
+ 'parent': None if badge['parent'] is None else all_badges[badge['parent']]
+ }
+ )
+ print("{} badge: {}".format("Created" if created else "Updated", badge['id_name']))
+
+ print("{} badges loaded!\n".format(len(all_badges)))
diff --git a/codewof/programming/migrations/0003_auto_20190904_1810.py b/codewof/programming/migrations/0003_auto_20190904_1810.py
new file mode 100644
index 000000000..d922845fe
--- /dev/null
+++ b/codewof/programming/migrations/0003_auto_20190904_1810.py
@@ -0,0 +1,51 @@
+# Generated by Django 2.1.5 on 2019-09-04 06:10
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('programming', '0002_auto_20190813_1548'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Badge',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('id_name', models.CharField(max_length=100, unique=True)),
+ ('display_name', models.CharField(max_length=100)),
+ ('description', models.CharField(max_length=500)),
+ ('icon_name', models.CharField(max_length=100, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Earned',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date', models.DateTimeField(default=django.utils.timezone.now)),
+ ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='programming.Badge')),
+ ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='programming.Profile')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Token',
+ fields=[
+ ('name', models.CharField(max_length=100, primary_key=True, serialize=False)),
+ ('token', models.CharField(max_length=500)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='attempt',
+ name='datetime',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ migrations.AddField(
+ model_name='profile',
+ name='earned_badges',
+ field=models.ManyToManyField(through='programming.Earned', to='programming.Badge'),
+ ),
+ ]
diff --git a/codewof/programming/migrations/0004_badge_badge_tier.py b/codewof/programming/migrations/0004_badge_badge_tier.py
new file mode 100644
index 000000000..fdf2cf1af
--- /dev/null
+++ b/codewof/programming/migrations/0004_badge_badge_tier.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.5 on 2019-09-19 02:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('programming', '0003_auto_20190904_1810'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='badge',
+ name='badge_tier',
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/codewof/programming/migrations/0005_badge_parent.py b/codewof/programming/migrations/0005_badge_parent.py
new file mode 100644
index 000000000..5a1530671
--- /dev/null
+++ b/codewof/programming/migrations/0005_badge_parent.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.1.5 on 2019-10-22 09:01
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('programming', '0004_badge_badge_tier'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='badge',
+ name='parent',
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='programming.Badge'),
+ ),
+ ]
diff --git a/codewof/programming/migrations/0006_profile_attempted_questions.py b/codewof/programming/migrations/0006_profile_attempted_questions.py
new file mode 100644
index 000000000..e13b17cc2
--- /dev/null
+++ b/codewof/programming/migrations/0006_profile_attempted_questions.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.5 on 2020-04-07 01:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('programming', '0005_badge_parent'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='profile',
+ name='attempted_questions',
+ field=models.ManyToManyField(through='programming.Attempt', to='programming.Question'),
+ ),
+ ]
diff --git a/codewof/programming/migrations/0007_auto_20200409_1424.py b/codewof/programming/migrations/0007_auto_20200409_1424.py
new file mode 100644
index 000000000..67d59dfb7
--- /dev/null
+++ b/codewof/programming/migrations/0007_auto_20200409_1424.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.1.5 on 2020-04-09 02:24
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('programming', '0006_profile_attempted_questions'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='earned',
+ options={'verbose_name': 'Earned badge', 'verbose_name_plural': 'Badges earned'},
+ ),
+ ]
diff --git a/codewof/programming/models.py b/codewof/programming/models.py
index 3f8a575cd..c5af929d9 100644
--- a/codewof/programming/models.py
+++ b/codewof/programming/models.py
@@ -7,6 +7,7 @@
from django.dispatch import receiver
from django.urls import reverse
from django.core.validators import MinValueValidator, MaxValueValidator
+from django.utils import timezone
from model_utils.managers import InheritanceManager
from utils.TranslatableModel import TranslatableModel
@@ -24,8 +25,8 @@ class Profile(models.Model):
default=1,
validators=[MinValueValidator(1), MaxValueValidator(7)]
)
- # earned_badges = models.ManyToManyField('Badge', through='Earned')
- # attempted_questions = models.ManyToManyField('Question', through='Attempt')
+ earned_badges = models.ManyToManyField('Badge', through='Earned')
+ attempted_questions = models.ManyToManyField('Question', through='Attempt')
def __str__(self):
"""Text representation of a profile."""
@@ -47,41 +48,52 @@ def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
-# class LoginDay(models.Model):
-# profile = models.ForeignKey('Profile', on_delete=models.CASCADE)
-# day = models.DateField(auto_now_add=True)
+class Badge(models.Model):
+ """Badge that can be earned by a user."""
-# def __str__(self):
-# return str(self.day)
+ id_name = models.CharField(max_length=SMALL, unique=True)
+ display_name = models.CharField(max_length=SMALL)
+ description = models.CharField(max_length=LARGE)
+ icon_name = models.CharField(null=True, max_length=SMALL)
+ badge_tier = models.IntegerField(default=0)
+ parent = models.ForeignKey('Badge', on_delete=models.CASCADE, null=True, default=None)
+ def __str__(self):
+ """Text representation of a badge."""
+ return self.display_name
-# class Badge(models.Model):
-# id_name = models.CharField(max_length=SMALL, unique=True)
-# display_name = models.CharField(max_length=SMALL)
-# description = models.CharField(max_length=LARGE)
-# def __str__(self):
-# return self.display_name
+class Earned(models.Model):
+ """Model that documents when a badge is earned by a user in their profile."""
+ profile = models.ForeignKey('Profile', on_delete=models.CASCADE)
+ badge = models.ForeignKey('Badge', on_delete=models.CASCADE)
+ date = models.DateTimeField(default=timezone.now)
-# class Earned(models.Model):
-# profile = models.ForeignKey('Profile', on_delete=models.CASCADE)
-# badge = models.ForeignKey('Badge', on_delete=models.CASCADE)
-# date = models.DateTimeField(auto_now_add=True)
+ class Meta:
+ """How the name is displayed in the Admin view."""
-# def __str__(self):
-# return str(self.date)
+ verbose_name = "Earned badge"
+ verbose_name_plural = "Badges earned"
-# class Token(models.Model):
-# name = models.CharField(max_length=SMALL, primary_key=True)
-# token = models.CharField(max_length=LARGE)
+ def __str__(self):
+ """Text representation of an Earned object."""
+ return str(self.date)
-# def __str__(self):
-# return self.name
+
+class Token(models.Model):
+ """Token model for codeWOF."""
+
+ name = models.CharField(max_length=SMALL, primary_key=True)
+ token = models.CharField(max_length=LARGE)
+
+ def __str__(self):
+ """Text representation of a Token."""
+ return self.name
class Attempt(models.Model):
- """An user attempt for a question."""
+ """A user attempt for a question."""
profile = models.ForeignKey(
'Profile',
@@ -95,9 +107,10 @@ class Attempt(models.Model):
'TestCase',
through='TestCaseAttempt'
)
- datetime = models.DateTimeField(auto_now_add=True)
+ datetime = models.DateTimeField(default=timezone.now)
user_code = models.TextField()
passed_tests = models.BooleanField(default=False)
+
# skills_hinted = models.ManyToManyField('Skill', blank=True)
def __str__(self):
@@ -147,9 +160,11 @@ def __str__(self):
else:
return self.title
- # class Meta:
- # verbose_name = "Parsons Problem"
- # verbose_name_plural = "All Questions & Parsons Problems"
+ class Meta:
+ """Meta information for class."""
+
+ verbose_name = 'Question'
+ verbose_name_plural = 'Questions'
class TestCase(TranslatableModel):
diff --git a/codewof/programming/urls.py b/codewof/programming/urls.py
index 7f0584aff..7ee9a611d 100644
--- a/codewof/programming/urls.py
+++ b/codewof/programming/urls.py
@@ -16,7 +16,6 @@
app_name = 'programming'
urlpatterns = [
- path('', views.IndexView.as_view(), name='home'),
path('', include(router.urls)),
path('questions/', views.QuestionListView.as_view(), name='question_list'),
path('questions/create/', views.CreateView.as_view(), name='create'),
diff --git a/codewof/programming/views-old.txt b/codewof/programming/views-old.txt
deleted file mode 100644
index 5d0237e49..000000000
--- a/codewof/programming/views-old.txt
+++ /dev/null
@@ -1,390 +0,0 @@
-# flake8: noqa
-# noqa
-
-from django.views import generic
-from django.http import JsonResponse, Http404
-from django.contrib import messages
-from django.contrib.auth import login, authenticate, update_session_auth_hash
-from django.contrib.auth.forms import PasswordChangeForm
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.core.exceptions import ObjectDoesNotExist
-from django.core import serializers
-import requests
-import time
-import datetime
-import random
-import json
-
-from codewof.models import (
- Profile,
- Question,
- TestCase,
- Attempt,
- TestCaseAttempt,
-)
-
-QUESTION_JAVASCRIPT = 'js/question_types/{}.js'
-
-
-class IndexView(generic.base.TemplateView):
- """Homepage for CodeWOF."""
-
- template_name = 'codewof/index.html'
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context['questions'] = Question.objects.select_subclasses()
-
- # if self.request.user.is_authenticated:
- # user = User.objects.get(username=self.request.user.username)
- # all_questions = Question.objects.all()
- # attempted_questions = user.profile.attempted_questions.all()
- # new_questions = all_questions.difference(attempted_questions)[:5]
-
- # history = []
- # for question in new_questions:
- # if question.title not in [question['title'] for question in history]:
- # history.append({'title': question.title, 'id': question.pk})
- # context['history'] = history
- return context
-
-# class LastAccessMixin(object):
-# def dispatch(self, request, *args, **kwargs):
-# """update days logged in when user accesses a page with this mixin"""
-# if request.user.is_authenticated:
-# request.user.last_login = datetime.datetime.now()
-# request.user.save(update_fields=['last_login'])
-
-# profile = request.user.profile
-# today = datetime.date.today()
-
-# login_days = profile.loginday_set.order_by('-day')
-# if len(login_days) > 1:
-# request.user.last_login = login_days[1].day
-# request.user.save(update_fields=['last_login'])
-
-# if not login_days.filter(day=today).exists():
-# day = LoginDay(profile=profile)
-# day.full_clean()
-# day.save()
-
-# return super(LastAccessMixin, self).dispatch(request, *args, **kwargs)
-
-# def get_random_question(request, current_question_id):
-# """redirect to random question user hasn't done, or to index page if there aren't any"""
-# valid_question_ids = []
-# if request.user.is_authenticated:
-# user = User.objects.get(username=request.user.username)
-# completed_questions = Question.objects.filter(profile=user.profile, attempt__passed_tests=True)
-# valid_question_ids = [question.id for question in Question.objects.all() if question not in completed_questions]
-# else:
-# valid_question_ids = [question.id for question in Question.objects.all()]
-
-# if current_question_id in valid_question_ids:
-# valid_question_ids.remove(current_question_id)
-
-# if len(valid_question_ids) < 1:
-# url = '/'
-# else:
-# question_number = random.choice(valid_question_ids)
-# url = '/questions/' + str(question_number)
-# return redirect(url)
-
-
-# def add_points(question, profile, passed_tests):
-# """add appropriate number of points (if any) to user's account"""
-# max_points_from_attempts = 3
-# points_for_correct = 10
-
-# n_attempts = len(Attempt.objects.filter(question=question, profile=profile, is_save=False))
-# previous_corrects = Attempt.objects.filter(question=question, profile=profile, passed_tests=True, is_save=False)
-# is_first_correct = len(previous_corrects) == 1
-
-# points_to_add = 0
-# if n_attempts <= max_points_from_attempts:
-# points_to_add += 1
-
-# if passed_tests and is_first_correct:
-# points_from_previous_attempts = n_attempts if n_attempts < max_points_from_attempts else max_points_from_attempts
-# points_to_add += (points_for_correct - points_from_previous_attempts)
-
-# profile.points += points_to_add
-# profile.full_clean()
-# profile.save()
-
-
-def save_question_attempt(request):
- """Save user's attempt for a question.
-
- If the attempt is successful: add points if these haven't already
- been added.
-
- Args:
- request (Request): AJAX request from user.
-
- Returns:
- JSON response with result.
- """
- result = {
- 'success': False,
- }
- if request.is_ajax():
- if request.user.is_authenticated:
- request_json = json.loads(request.body.decode('utf-8'))
- profile = request.user.profile
- question = Question.objects.get(pk=request_json['question'])
- user_code = request_json['user_input']
-
- test_cases = request_json['test_cases']
- total_tests = len(test_cases)
- total_passed = 0
- for test_case in test_cases.values():
- if test_case['passed']:
- total_passed += 1
-
- attempt = Attempt.objects.create(
- profile=profile,
- question=question,
- user_code=user_code,
- passed_tests=total_passed == total_tests,
- )
-
- # Create test case attempt objects
- for test_case_id, test_case_data in test_cases.items():
- test_case = TestCase.objects.get(pk=test_case_id)
- TestCaseAttempt.objects.create(
- attempt=attempt,
- test_case=test_case,
- passed=test_case_data['passed'],
- )
-
- result['success'] = True
-
- return JsonResponse(result)
-
-# 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 = User.objects.get(username=request.user.username)
-# profile = user.profile
-
-# goal_choice = request_json['goal_choice']
-# profile.goal = int(goal_choice)
-# profile.full_clean()
-# profile.save()
-
-# return JsonResponse({})
-
-
-# def get_consecutive_sections(days_logged_in):
-# """return a list of lists of consecutive days logged in"""
-# consecutive_sections = []
-
-# today = days_logged_in[0]
-# previous_section = [today]
-# for day in days_logged_in[1:]:
-# if day == previous_section[-1] - datetime.timedelta(days=1):
-# previous_section.append(day)
-# else:
-# consecutive_sections.append(previous_section)
-# previous_section = [day]
-
-# consecutive_sections.append(previous_section)
-# return consecutive_sections
-
-
-# def check_badge_conditions(user):
-# """check badges for account creation, days logged in, and questions solved"""
-# earned_badges = user.profile.earned_badges.all()
-
-# # account creation badge
-# try:
-# creation_badge = Badge.objects.get(id_name="create-account")
-# if creation_badge not in earned_badges:
-# new_achievement = Earned(profile=user.profile, badge=creation_badge)
-# new_achievement.full_clean()
-# new_achievement.save()
-# except (Badge.DoesNotExist):
-# pass
-
-# # consecutive days logged in badges
-# login_badges = Badge.objects.filter(id_name__contains="login")
-# for login_badge in login_badges:
-# if login_badge not in earned_badges:
-# n_days = int(login_badge.id_name.split("-")[1])
-
-# days_logged_in = LoginDay.objects.filter(profile=user.profile)
-# days_logged_in = sorted(days_logged_in, key=lambda k: k.day, reverse=True)
-# sections = get_consecutive_sections([d.day for d in days_logged_in])
-
-# max_consecutive = len(max(sections, key=lambda k: len(k)))
-
-# if max_consecutive >= n_days:
-# new_achievement = Earned(profile=user.profile, badge=login_badge)
-# new_achievement.full_clean()
-# new_achievement.save()
-
-# # solved questions badges
-# solve_badges = Badge.objects.filter(id_name__contains="solve")
-# for solve_badge in solve_badges:
-# if solve_badge not in earned_badges:
-# n_problems = int(solve_badge.id_name.split("-")[1])
-# n_completed = Attempt.objects.filter(profile=user.profile, passed_tests=True, is_save=False)
-# n_distinct = n_completed.values("question__pk").distinct().count()
-# if n_distinct >= n_problems:
-# new_achievement = Earned(profile=user.profile, badge=solve_badge)
-# new_achievement.full_clean()
-# new_achievement.save()
-
-
-# def get_past_5_weeks(user):
-# """get how many questions a user has done each week for the last 5 weeks"""
-# t = datetime.date.today()
-# today = datetime.datetime(t.year, t.month, t.day)
-# last_monday = today - datetime.timedelta(days=today.weekday(), weeks=0)
-# last_last_monday = today - datetime.timedelta(days=today.weekday(), weeks=1)
-
-# past_5_weeks = []
-# to_date = today
-# for week in range(0, 5):
-# from_date = today - datetime.timedelta(days=today.weekday(), weeks=week)
-# attempts = Attempt.objects.filter(profile=user.profile, date__range=(from_date, to_date + datetime.timedelta(days=1)), is_save=False)
-# distinct_questions_attempted = attempts.values("question__pk").distinct().count()
-
-# label = str(week) + " weeks ago"
-# if week == 0:
-# label = "This week"
-# elif week == 1:
-# label = "Last week"
-
-# past_5_weeks.append({'week': from_date, 'n_attempts': distinct_questions_attempted, 'label': label})
-# to_date = from_date
-# return past_5_weeks
-
-
-class ProfileView(LoginRequiredMixin, generic.DetailView):
- """Displays a user's profile."""
-
- login_url = '/login/'
- redirect_field_name = 'next'
- template_name = 'codewof/profile.html'
- model = Profile
-
- def get_object(self):
- if self.request.user.is_authenticated:
- return Profile.objects.get(user=self.request.user)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
-
- # user = User.objects.get(username=self.request.user.username)
- # questions = user.profile.attempted_questions.all()
-
- # check_badge_conditions(user)
-
- # context['goal'] = user.profile.goal
- # context['all_badges'] = Badge.objects.all()
- # context['past_5_weeks'] = get_past_5_weeks(user)
-
- # history = []
- # for question in questions:
- # if question.title not in [question['title'] for question in history]:
- # attempts = Attempt.objects.filter(profile=user.profile, question=question, is_save=False)
- # if len(attempts) > 0:
- # max_date = max(attempt.date for attempt in attempts)
- # completed = any(attempt.passed_tests for attempt in attempts)
- # history.append({'latest_attempt': max_date,'title': question.title,'n_attempts': len(attempts), 'completed': completed, 'id': question.pk})
- # context['history'] = sorted(history, key=lambda k: k['latest_attempt'], reverse=True)
- return context
-
-
-# class SkillView(LastAccessMixin, generic.DetailView):
-# """displays list of questions which involve this skill"""
-# template_name = 'codewof/skill.html'
-# context_object_name = 'skill'
-# model = SkillArea
-
-# def get_context_data(self, **kwargs):
-# context = super().get_context_data(**kwargs)
-
-# skill = self.get_object()
-# questions = skill.questions.all()
-# context['questions'] = questions
-
-# if self.request.user.is_authenticated:
-# user = User.objects.get(username=self.request.user.username)
-
-# history = []
-# for question in questions:
-# if question.title not in [question['title'] for question in history]:
-# attempts = Attempt.objects.filter(profile=user.profile, question=question, is_save=False)
-# attempted = False
-# completed = False
-# if len(attempts) > 0:
-# attempted = True
-# completed = any(attempt.passed_tests for attempt in attempts)
-# history.append(
-# {
-# 'attempted': attempted,
-# 'completed': completed,
-# 'title': question.title,
-# 'id': question.pk
-# }
-# )
-# context['questions'] = history
-# return context
-
-
-class QuestionListView(generic.ListView):
- """View for listing questions."""
-
- model = Question
- context_object_name = 'questions'
-
- def get_queryset(self):
- """Return questions objects for page.
-
- Returns:
- Question queryset.
- """
- return Question.objects.all().select_subclasses()
-
-
-class QuestionView(generic.base.TemplateView):
- """Displays a question.
-
- This view requires to retrieve the object first in the context,
- in order to determine the required template to render.
- """
-
- template_name = 'codewof/question.html'
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- try:
- self.question = Question.objects.get_subclass(
- pk=self.kwargs['pk']
- )
- except Question.DoesNotExist:
- raise Http404("No question matches the given ID.")
- context['question'] = self.question
- test_cases = self.question.test_cases.values()
- context['test_cases'] = test_cases
- context['test_cases_json'] = json.dumps(list(test_cases))
- context['question_js'] = QUESTION_JAVASCRIPT.format(self.question.QUESTION_TYPE)
-
- if self.request.user.is_authenticated:
- try:
- previous_attempt = Attempt.objects.filter(
- profile=self.request.user.profile,
- question=self.question,
- ).latest('datetime')
- except ObjectDoesNotExist:
- previous_attempt = None
- context['previous_attempt'] = previous_attempt
- # all_attempts = Attempt.objects.filter(question=question, profile=profile)
- # if len(all_attempts) > 0:
- # context['previous_attempt'] = all_attempts.latest('date').user_code
- return context
diff --git a/codewof/programming/views.py b/codewof/programming/views.py
index 6a97f5cb8..0ce2d6bf1 100644
--- a/codewof/programming/views.py
+++ b/codewof/programming/views.py
@@ -24,19 +24,9 @@
)
from research.models import StudyRegistration
-QUESTION_JAVASCRIPT = 'js/question_types/{}.js'
-
-
-class IndexView(generic.base.TemplateView):
- """Homepage for programming."""
-
- template_name = 'programming/index.html'
+from programming.codewof_utils import add_points, check_badge_conditions
- def get_context_data(self, **kwargs):
- """Get additional context data for template."""
- context = super().get_context_data(**kwargs)
- context['questions'] = Question.objects.select_subclasses()
- return context
+QUESTION_JAVASCRIPT = 'js/question_types/{}.js'
class QuestionListView(LoginRequiredMixin, generic.ListView):
@@ -184,6 +174,13 @@ def save_question_attempt(request):
passed=test_case_data['passed'],
)
result['success'] = True
+ points_before = profile.points
+ points = add_points(question, profile, attempt)
+ badges = check_badge_conditions(profile.user)
+ points_after = profile.points
+ result['curr_points'] = points
+ result['point_diff'] = points_after - points_before
+ result['badges'] = badges
else:
result['success'] = False
result['message'] = 'Attempt not saved, same as previous attempt.'
@@ -218,6 +215,26 @@ def get_context_data(self, **kwargs):
return context
+# class ProfileView(LoginRequiredMixin, generic.DetailView):
+# """Displays a user's profile."""
+
+# login_url = '/login/'
+# redirect_field_name = 'next'
+# template_name = 'users/user_detail.html'
+# model = Profile
+
+# def get_context_data(self, **kwargs):
+# """Get additional context data for template."""
+# context = super().get_context_data(**kwargs)
+
+# user = self.request.user
+# context['goal'] = user.profile.goal
+# context['all_badges'] = Badge.objects.all()
+# check_badge_conditions(user)
+# # context['past_5_weeks'] = get_past_5_weeks(user)
+# return context
+
+
class QuestionAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint that allows questions to be viewed."""
diff --git a/codewof/static/img/icons/badges/icons8-attempt-made-black-50.png b/codewof/static/img/icons/badges/icons8-attempt-made-black-50.png
new file mode 100644
index 000000000..074c96897
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-attempt-made-black-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-attempt-made-bronze-50.png b/codewof/static/img/icons/badges/icons8-attempt-made-bronze-50.png
new file mode 100644
index 000000000..0f831bac4
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-attempt-made-bronze-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-attempt-made-gold-50.png b/codewof/static/img/icons/badges/icons8-attempt-made-gold-50.png
new file mode 100644
index 000000000..17ceaa413
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-attempt-made-gold-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-attempt-made-silver-50.png b/codewof/static/img/icons/badges/icons8-attempt-made-silver-50.png
new file mode 100644
index 000000000..6a6b21f1c
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-attempt-made-silver-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-badge-create-account-48.png b/codewof/static/img/icons/badges/icons8-badge-create-account-48.png
new file mode 100644
index 000000000..9fc2774e5
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-badge-create-account-48.png differ
diff --git a/codewof/static/img/icons/badges/icons8-calendar-14-50.png b/codewof/static/img/icons/badges/icons8-calendar-14-50.png
new file mode 100644
index 000000000..b609e7afd
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-calendar-14-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-calendar-2-50.png b/codewof/static/img/icons/badges/icons8-calendar-2-50.png
new file mode 100644
index 000000000..79a690d87
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-calendar-2-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-calendar-21-50.png b/codewof/static/img/icons/badges/icons8-calendar-21-50.png
new file mode 100644
index 000000000..054b6c231
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-calendar-21-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-calendar-28-50.png b/codewof/static/img/icons/badges/icons8-calendar-28-50.png
new file mode 100644
index 000000000..e532ffa29
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-calendar-28-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-calendar-7-50.png b/codewof/static/img/icons/badges/icons8-calendar-7-50.png
new file mode 100644
index 000000000..ae1d27761
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-calendar-7-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-question-solved-black-50.png b/codewof/static/img/icons/badges/icons8-question-solved-black-50.png
new file mode 100644
index 000000000..ca2659f3f
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-question-solved-black-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-question-solved-bronze-50.png b/codewof/static/img/icons/badges/icons8-question-solved-bronze-50.png
new file mode 100644
index 000000000..a40520bd2
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-question-solved-bronze-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-question-solved-gold-50.png b/codewof/static/img/icons/badges/icons8-question-solved-gold-50.png
new file mode 100644
index 000000000..f7a726f60
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-question-solved-gold-50.png differ
diff --git a/codewof/static/img/icons/badges/icons8-question-solved-silver-50.png b/codewof/static/img/icons/badges/icons8-question-solved-silver-50.png
new file mode 100644
index 000000000..e3d1d5e34
Binary files /dev/null and b/codewof/static/img/icons/badges/icons8-question-solved-silver-50.png differ
diff --git a/codewof/static/img/icons/icons8-star-64.png b/codewof/static/img/icons/icons8-star-64.png
new file mode 100644
index 000000000..2abeca3dc
Binary files /dev/null and b/codewof/static/img/icons/icons8-star-64.png differ
diff --git a/codewof/static/js/question_types/base.js b/codewof/static/js/question_types/base.js
index 50bc83d22..304d50c1d 100644
--- a/codewof/static/js/question_types/base.js
+++ b/codewof/static/js/question_types/base.js
@@ -1,4 +1,7 @@
+$ = jQuery = require('jquery');
require('skulpt');
+require('bootstrap');
+require('details-element-polyfill');
function ajax_request(url_name, data, success_function) {
$.ajax({
@@ -9,7 +12,7 @@ function ajax_request(url_name, data, success_function) {
contentType: 'application/json; charset=utf-8',
headers: { "X-CSRFToken": csrf_token },
dataType: 'json',
- success: success_function
+ success: update_gamification
});
}
@@ -25,6 +28,31 @@ function clear_submission_feedback() {
$('#submission_feedback').empty();
}
+function update_gamification(data) {
+ curr_points = data.curr_points;
+ $('#user_points_navbar').innerText = curr_points;
+ $("#user_points_navbar").load(location.href + " #user_points_navbar"); // Add space between URL and selector.
+
+ point_diff = parseInt(data.point_diff);
+ if(point_diff > 0) {
+ $("#point_toast_header").text("Points earned!");
+ $("#point_toast_body").text("You earned " + point_diff.toString() +" points!");
+ $(document).ready(function(){
+ $("#point_toast").toast('show', {delay: 3000});
+ });
+ }
+
+ badges = data.badges;
+ if (badges.length > 0){
+ $("#badge_toast_header").text("New badges!");
+ $("#badge_toast_body").text(badges);
+ $(document).ready(function(){
+ $("#badge_toast").toast('show', {delay: 3000});
+ });
+ }
+
+}
+
function display_submission_feedback(test_cases) {
var container = $('#submission_feedback');
var total_tests = Object.keys(test_cases).length;
@@ -43,7 +71,7 @@ function display_submission_feedback(test_cases) {
text = 'Great work! All the tests passed.';
container.append(create_alert('success', text));
} else {
- text = 'Oh no! It seems like some of the tests failed. Try to figure out why, and then try again.';
+ text = 'Oh no! It seems like some of the tests did not pass. Try to figure out why, and then try again.';
container.append(create_alert('danger', text));
}
}
diff --git a/codewof/static/scss/website.scss b/codewof/static/scss/website.scss
index 343b53f16..1aaa4b8e8 100644
--- a/codewof/static/scss/website.scss
+++ b/codewof/static/scss/website.scss
@@ -178,6 +178,26 @@ strong {
}
}
+.badge-container {
+ clear: both;
+ padding-bottom: 0.5rem;
+}
+
+.badge-icon {
+ padding: 0.3125rem;
+ float: left;
+}
+
+.badge-icon-unachieved {
+ padding: 0.3125rem;
+ opacity: 0.2;
+}
+
+.achievements-link,
+.achievements-link:hover {
+ color: inherit;
+}
+
.img-inline {
max-height: 1.5rem;
}
@@ -248,3 +268,33 @@ $red: #b94a48;
.orange-underline {
border-bottom: 0.5rem $brand-secondary-colour solid;
}
+
+#toast-container {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+.toast {
+ border: 1px solid $brand-colour;
+ background-color: brand-colour-light;
+ text-align: center;
+}
+
+.toast-header {
+ border: 1px solid $brand-colour;
+ background-color: brand-colour-light;
+ width: 100%;
+ font-weight: bold;
+ display: inline-block;
+}
+
+.toast-header strong {
+ color: black;
+ font-weight: bold;
+}
+
+.toast-body {
+ border: 1px solid $brand-colour;
+ background-color: brand-colour-light;
+}
diff --git a/codewof/static/svg/icons8-points.svg b/codewof/static/svg/icons8-points.svg
new file mode 100644
index 000000000..a6d33102c
--- /dev/null
+++ b/codewof/static/svg/icons8-points.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/codewof/templates/base.html b/codewof/templates/base.html
index 19db2b69b..1feb5dabd 100644
--- a/codewof/templates/base.html
+++ b/codewof/templates/base.html
@@ -52,11 +52,14 @@
{% if request %}
{% activeurl %}
-
+
{% if request and request.user.is_authenticated %}
Questions
{# URL provided by django-allauth/account/urls.py #}
+ {% svg 'icons8-points' %}
+ {{request.user.profile.points}}
+ {# URL provided by django-allauth/account/urls.py #}
{% svg 'icons8-user' %}
{{ request.user.get_short_name }}'s Dashboard
{# URL provided by django-allauth/account/urls.py #}
diff --git a/codewof/templates/programming/badge_card.html b/codewof/templates/programming/badge_card.html
new file mode 100644
index 000000000..28f597716
--- /dev/null
+++ b/codewof/templates/programming/badge_card.html
@@ -0,0 +1,15 @@
+{% load static %}
+
+
diff --git a/codewof/templates/programming/question.html b/codewof/templates/programming/question.html
index 922e349e3..e34abc964 100644
--- a/codewof/templates/programming/question.html
+++ b/codewof/templates/programming/question.html
@@ -61,6 +61,31 @@ {{ question.title }}
+
+
+
+
+ Check your dashboard for details!
+
+
+
+
+
+ Check your dashboard for details!
+
+
+
{% endblock %}
{% block scripts %}
diff --git a/codewof/templates/users/achievements.html b/codewof/templates/users/achievements.html
new file mode 100644
index 000000000..4979d4cd6
--- /dev/null
+++ b/codewof/templates/users/achievements.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}Achievements{% endblock %}
+
+{% block content %}
+
+
Achievements
+
Achievements earned: {{num_badges_earned}}/{{num_badges}}
+
+
+
+ {% for badge in user.profile.earned_badges.all %}
+
+
+
{{badge.display_name}}
+
+ {% endfor %}
+ {% for badge in badges_not_earned %}
+
+
+
+ {% endfor %}
+
+
+
+{% endblock content %}
diff --git a/codewof/templates/users/activity_graph.html b/codewof/templates/users/activity_graph.html
new file mode 100644
index 000000000..7fc08c960
--- /dev/null
+++ b/codewof/templates/users/activity_graph.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+ {% for day in days_used %}
+ {% if day is False %}
+
+ {% else %}
+
1
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/codewof/templates/users/dashboard.html b/codewof/templates/users/dashboard.html
index 05dbf9575..32b4602f5 100644
--- a/codewof/templates/users/dashboard.html
+++ b/codewof/templates/users/dashboard.html
@@ -30,11 +30,23 @@ Your questions for today
Sorry! We currently don't have questions for you to do today.
{% endif %}
+ You've solved {{num_questions_answered}} question{{ num_questions_answered|pluralize }} in the last month!
-
Achievements
-
Coming soon!
+
Points earned: {{user.profile.points}}
+
Achievements
+
+ {% for badge in all_badges %}
+ {% if badge in user.profile.earned_badges.all and badge.parent not in user.profile.earned_badges.all %}
+
+ {% endif %}
+ {% endfor %}
diff --git a/codewof/templates/users/user_detail.html b/codewof/templates/users/user_detail.html
new file mode 100644
index 000000000..14941231c
--- /dev/null
+++ b/codewof/templates/users/user_detail.html
@@ -0,0 +1,48 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}User: {{ user.get_full_name }}{% endblock %}
+
+{% block content %}
+
+
+
+
+
{{ user.get_full_name }}
+
+
Date joined: {{ user.date_joined }}
+
Points earned: {{user.profile.points}}
+
Achievements:
+
+ {% for badge in all_badges %}
+ {% if badge in user.profile.earned_badges.all %}
+
+
{{badge.display_name}}
+ {% else %}
+
{{badge.display_name}}
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% endblock content %}
diff --git a/codewof/tests/codewof_test_data_generator.py b/codewof/tests/codewof_test_data_generator.py
new file mode 100644
index 000000000..a16e8c89f
--- /dev/null
+++ b/codewof/tests/codewof_test_data_generator.py
@@ -0,0 +1,133 @@
+"""Class to generate test data required for testing codewof system."""
+
+from django.contrib.auth import get_user_model
+from django.core import management
+import datetime
+
+from programming.models import (
+ Question,
+ Attempt,
+ Badge,
+ QuestionTypeProgram,
+ QuestionTypeFunction,
+ QuestionTypeParsons,
+ QuestionTypeDebugging,
+)
+
+from users.models import UserType
+
+User = get_user_model()
+
+
+def generate_questions():
+ """Generate questions for use in codeWOF tests. Questions contain minimum information and complexity."""
+ Question.objects.create(slug="question-1", title='Test', question_text='Hello')
+
+ QuestionTypeProgram.objects.create(
+ slug="program-question-1",
+ title='Test',
+ question_text='Hello',
+ solution="question_answer"
+ )
+
+ QuestionTypeFunction.objects.create(
+ slug="function-question-1",
+ title='Test',
+ question_text='Hello',
+ solution="question_answer"
+ )
+
+ QuestionTypeParsons.objects.create(
+ slug="parsons-question-1",
+ title='Test',
+ question_text='Hello',
+ solution="question_answer",
+ lines="These are\nthe lines"
+ )
+
+ QuestionTypeDebugging.objects.create(
+ slug="debugging-question-1",
+ title='Test',
+ question_text='Hello',
+ solution="question_answer",
+ initial_code=''
+ )
+
+
+def generate_users(user):
+ """Generate users for codeWOF tests. Creates two basic users for unit tests."""
+ management.call_command("load_user_types")
+ user_john = User.objects.create_user(
+ id=1,
+ username='john',
+ first_name='John',
+ last_name='Doe',
+ email='john@uclive.ac.nz',
+ password='onion',
+ user_type=UserType.objects.get(slug='student')
+ )
+ user_john.save()
+
+ user_sally = User.objects.create_user(
+ id=2,
+ username='sally',
+ first_name='Sally',
+ last_name='Jones',
+ email='sally@uclive.ac.nz',
+ password='onion',
+ user_type=UserType.objects.get(slug='other')
+ )
+ user_sally.save()
+
+
+def generate_badges():
+ """Create badges for codeWOF tests. Badges created for each main current badge category."""
+ Badge.objects.create(
+ id_name='questions-solved-1',
+ display_name='Solved one question',
+ description='first',
+ badge_tier=1,
+ )
+ Badge.objects.create(
+ id_name='create-account',
+ display_name='Account created',
+ description='test',
+ badge_tier=0,
+ )
+ Badge.objects.create(
+ id_name='attempts-made-5',
+ display_name='Five attempts made',
+ description='test',
+ badge_tier=2
+ )
+ Badge.objects.create(
+ id_name='attempts-made-1',
+ display_name='One attempt made',
+ description='test',
+ badge_tier=1,
+ parent=Badge.objects.get(id_name='attempts-made-5')
+ )
+ Badge.objects.create(
+ id_name='consecutive-days-2',
+ display_name='Two consecutive days',
+ description='test',
+ badge_tier=1,
+ )
+
+
+def generate_attempts():
+ """
+ Generate attempts for codeWOF tests.
+
+ Attempts are generated for user 1 and question 1, with attempts created to cover consecutive days, failed attempts,
+ and passed attempts. These attempts cover the main requirements to gain all test badges.
+ """
+ user = User.objects.get(id=1)
+ question = Question.objects.get(slug='question-1')
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True)
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=False)
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=False)
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True,
+ datetime=datetime.date(2019, 9, 9))
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True,
+ datetime=datetime.date(2019, 9, 10))
diff --git a/codewof/tests/features/badges.feature b/codewof/tests/features/badges.feature
new file mode 100644
index 000000000..71fa92bd2
--- /dev/null
+++ b/codewof/tests/features/badges.feature
@@ -0,0 +1,27 @@
+Feature: Badges
+
+#check badge on creation
+Scenario: User profile is created
+ Given a user with ID 1 exists
+ When their account is created
+ Then their profile has 1 badge
+
+Scenario: User profile earns badge for one attempt
+ Given a user with ID 1 exists
+ And they have not earned the badge for one attempt
+ When the user attempts the "Print CodeWOF" question without solving it
+ Then the user earns a badge for one attempt made
+
+Scenario: User profile earns badge for one question solved
+ Given a user with ID 1 exists
+ And they have not earned the badge for solving one question
+ When the user solves the "Print CodeWOF" question
+ Then the user earns a badge for one question solved
+
+Scenario: User profile earns two badges on first question
+ Given a user with ID 1 exists
+ And they have not earned the badge for solving one question
+ And they have not attempted the "Print CodeWOF" question
+ And they have not earned the badge for one attempt
+ When the user solves the "Print CodeWOF" question
+ Then the user earns the one question solved badge and the one attempt made badge
diff --git a/codewof/tests/features/points.feature b/codewof/tests/features/points.feature
new file mode 100644
index 000000000..8d2bea989
--- /dev/null
+++ b/codewof/tests/features/points.feature
@@ -0,0 +1,55 @@
+Feature: Points
+
+#Test initial points are zero
+Scenario: User profile is created
+ Given a user with ID 1 is logged in
+ When their account is created
+ Then their profile's points are equal to 0
+
+#Test bonus point for first correct login
+Scenario: User solves a question on first attempt
+ Given a user with ID 1 is logged in
+ And their profile points are 0
+ And they have not attempted the "Print CodeWOF" question
+ When they solve the "Print CodeWOF" question
+ Then the user's points equal 12
+
+#Test for question that has previously been attempted
+Scenario: User solves an attempted question
+ Given a user with ID 1 is logged in
+ And their profile points are 0
+ And they have attempted the "Print CodeWOF" question
+ When they solve the "Print CodeWOF" question
+ Then the user's points equal 10
+
+#Test for question that has been attempted but not solved
+Scenario: User attempts a question
+ Given a user with ID 1 is logged in
+ And their profile points are 0
+ When they attempt the "Print CodeWOF" question without solving it
+ Then the user's points equal 0
+
+#Test for question that has previously been solved
+Scenario: User attempts a question
+ Given a user with ID 1 is logged in
+ And their profile points are 10
+ And they have already solved the "Print CodeWOF" question
+ When they solve the "Print CodeWOF" question
+ Then the user's points equal 10
+
+#Test for points earned with first attempt badge
+Scenario: User earns badge for one attempt
+ Given a user with ID 1 is logged in
+ And their profile points are 0
+ And they have not earned the badge for attempting one question
+ When they attempt the "Print CodeWOF" question
+ Then the user's points equal 10
+
+ #Test for points earned with five attempts badge
+ Scenario: User earns badge for five attempts
+ Given a user with ID 1 is logged in
+ And their profile points are 10
+ And they have not earned the badge for attempting five questions
+ And they have made 4 attempts in total
+ When they attempt the "Print CodeWOF" question
+ Then the user's points equal 30
diff --git a/codewof/tests/programming/factories.py b/codewof/tests/programming/factories.py
index b942ec40a..e103fcc09 100644
--- a/codewof/tests/programming/factories.py
+++ b/codewof/tests/programming/factories.py
@@ -8,6 +8,7 @@
post_generation,
)
from programming.models import Question, Profile, Attempt
+from django.utils import timezone
# shuffle the quesitons so it doesn't appear as 1, 2, 3, 4...
question_list = list(Question.objects.all())
@@ -18,7 +19,7 @@ class AttemptFactory(DjangoModelFactory):
"""Factory for generating attempts."""
profile = Iterator(Profile.objects.all())
- datetime = Faker('iso8601')
+ datetime = Faker('date_time', tzinfo=timezone.get_current_timezone())
question = Iterator(question_list)
user_code = Faker('paragraph', nb_sentences=5)
diff --git a/codewof/tests/programming/test_codewof_utils.py b/codewof/tests/programming/test_codewof_utils.py
new file mode 100644
index 000000000..42935dc74
--- /dev/null
+++ b/codewof/tests/programming/test_codewof_utils.py
@@ -0,0 +1,169 @@
+from django.test import TestCase
+from django.contrib.auth import get_user_model
+
+from programming.models import (
+ Question,
+ Attempt,
+ Badge,
+ Earned,
+)
+from codewof.tests.codewof_test_data_generator import (
+ generate_users,
+ generate_badges,
+ generate_questions,
+ generate_attempts,
+)
+from programming.codewof_utils import (
+ add_points,
+ backdate_points,
+ backdate_points_and_badges,
+ calculate_badge_points,
+ check_badge_conditions,
+ get_days_consecutively_answered,
+ get_questions_answered_in_past_month,
+ POINTS_BADGE,
+ POINTS_SOLUTION,
+ POINTS_BONUS,
+)
+from codewof.tests.conftest import user
+
+User = get_user_model()
+
+
+class TestCodewofUtils(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_users(user)
+ generate_questions()
+ generate_badges()
+
+ def test_add_points_first_attempt_correct(self):
+ user = User.objects.get(id=1)
+ question = Question.objects.get(id=1)
+ attempt = Attempt.objects.create(
+ profile=user.profile,
+ question=question,
+ passed_tests=True
+ )
+ points_before = user.profile.points
+ points_after = add_points(question, user.profile, attempt)
+ self.assertEqual(points_after - points_before, POINTS_SOLUTION + POINTS_BONUS)
+
+ def test_add_points_first_attempt_incorrect(self):
+ user = User.objects.get(id=1)
+ question = Question.objects.get(id=1)
+ attempt_1 = Attempt.objects.create(
+ profile=user.profile,
+ question=question,
+ passed_tests=False
+ )
+ points_before = user.profile.points
+ points_after = add_points(question, user.profile, attempt_1)
+ self.assertEqual(points_after - points_before, 0)
+
+ attempt_2 = Attempt.objects.create(
+ profile=user.profile,
+ question=question,
+ passed_tests=True
+ )
+ points_before = user.profile.points
+ points_after = add_points(question, user.profile, attempt_2)
+ self.assertEqual(points_after - points_before, POINTS_SOLUTION)
+
+ def test_calculate_badge_points_tier_0(self):
+ user = User.objects.get(id=1)
+ badge = Badge.objects.get(id_name="create-account")
+ badges = [badge]
+
+ points_before = user.profile.points
+ calculate_badge_points(user, badges)
+ self.assertEqual(user.profile.points - points_before, badge.badge_tier * POINTS_BADGE)
+
+ def test_calculate_badge_points_tier_1(self):
+ user = User.objects.get(id=1)
+ badge = Badge.objects.get(id_name="questions-solved-1")
+ badges = [badge]
+
+ points_before = user.profile.points
+ calculate_badge_points(user, badges)
+ self.assertEqual(user.profile.points - points_before, badge.badge_tier * POINTS_BADGE)
+
+ def test_calculate_badge_points_tier_2(self):
+ user = User.objects.get(id=1)
+ badge = Badge.objects.get(id_name="attempts-made-5")
+ badges = [badge]
+
+ points_before = user.profile.points
+ calculate_badge_points(user, badges)
+ self.assertEqual(user.profile.points - points_before, badge.badge_tier * POINTS_BADGE)
+
+ def test_check_badge_conditions(self):
+ generate_attempts()
+ user = User.objects.get(id=1)
+ self.assertEqual(user.profile.earned_badges.count(), 0)
+ check_badge_conditions(user)
+ earned_badges = user.profile.earned_badges
+ self.assertTrue(earned_badges.filter(id_name='create-account').exists())
+ self.assertTrue(earned_badges.filter(id_name='attempts-made-1').exists())
+ self.assertTrue(earned_badges.filter(id_name='attempts-made-5').exists())
+ self.assertTrue(earned_badges.filter(id_name='questions-solved-1').exists())
+ self.assertTrue(earned_badges.filter(id_name='consecutive-days-2').exists())
+
+ def test_get_days_consecutively_answered(self):
+ generate_attempts()
+ user = User.objects.get(id=1)
+ streak = get_days_consecutively_answered(user)
+ self.assertEqual(streak, 2)
+
+ def test_get_questions_answered_in_past_month(self):
+ generate_attempts()
+ user = User.objects.get(id=1)
+ num_solved = get_questions_answered_in_past_month(user)
+ self.assertEqual(num_solved, 1)
+
+ def test_backdate_points_correct_second_attempt(self):
+ user = User.objects.get(id=2)
+ question = Question.objects.get(slug='question-1')
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=False)
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True)
+ profile = backdate_points(user.profile)
+ self.assertEqual(profile.points, 10)
+
+ def test_backdate_points_correct_multiple_attempts(self):
+ user = User.objects.get(id=2)
+ question = Question.objects.get(slug='question-1')
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True)
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True)
+ profile = backdate_points(user.profile)
+ self.assertEqual(profile.points, 12)
+
+ def test_backdate_points_and_badges_too_many_points(self):
+ generate_attempts()
+ user = User.objects.get(id=1)
+ user.profile.points = 1000
+ backdate_points_and_badges()
+ self.assertEqual(User.objects.get(id=1).profile.points, 62)
+
+ def test_backdate_points_and_badges_run_twice(self):
+ generate_attempts()
+ user = User.objects.get(id=1)
+ user.profile.points = 1000
+ backdate_points_and_badges()
+ backdate_points_and_badges()
+ self.assertEqual(User.objects.get(id=1).profile.points, 62)
+ earned_badges = User.objects.get(id=1).profile.earned_badges
+ self.assertEqual(len(earned_badges.filter(id_name='create-account')), 1)
+ self.assertEqual(len(earned_badges.filter(id_name='attempts-made-1')), 1)
+ self.assertEqual(len(earned_badges.filter(id_name='attempts-made-5')), 1)
+ self.assertEqual(len(earned_badges.filter(id_name='questions-solved-1')), 1)
+ self.assertEqual(len(earned_badges.filter(id_name='consecutive-days-2')), 1)
+
+ def test_backdate_points_and_badges_badge_earnt_no_longer_meets_requirements(self):
+ user = User.objects.get(id=2)
+ badge = Badge.objects.get(id_name='attempts-made-5')
+ Earned.objects.create(profile=user.profile, badge=badge)
+ backdate_points_and_badges()
+ self.assertTrue(
+ User.objects.get(id=2).profile.earned_badges.filter(id_name='attempts-made-5').exists()
+ )
diff --git a/codewof/tests/programming/test_models.py b/codewof/tests/programming/test_models.py
index 99568fbfd..2441a1083 100644
--- a/codewof/tests/programming/test_models.py
+++ b/codewof/tests/programming/test_models.py
@@ -1,153 +1,454 @@
-# from django.test import TestCase
-# from django.core.exceptions import ValidationError
-# from django.db.utils import IntegrityError
-# from django.contrib.auth.models import User
-
-# from questions.models import Token, Badge, Profile, Question, Programming, ProgrammingFunction, Buggy, BuggyFunction
-
-
-# class TokenModelTests(TestCase):
-# @classmethod
-# def setUpTestData(cls):
-# Token.objects.create(name='sphere', token='abc')
-
-# def test_name_unique(self):
-# with self.assertRaises(IntegrityError):
-# Token.objects.create(name='sphere', token='def')
-
-# class BadgeModelTests(TestCase):
-# @classmethod
-# def setUpTestData(cls):
-# Badge.objects.create(id_name='solve-40', display_name='first', description='first')
-
-# def test_id_name_unique(self):
-# with self.assertRaises(IntegrityError):
-# Badge.objects.create(id_name='solve-40', display_name='second', description='second')
-
-# class ProfileModelTests(TestCase):
+from django.test import TestCase
+from django.core.exceptions import ValidationError
+from django.db.utils import IntegrityError
+from django.contrib.auth import get_user_model
+from programming.codewof_utils import check_badge_conditions
+from programming.models import (
+ Token,
+ Badge,
+ Question,
+ Earned,
+ Attempt,
+ QuestionTypeProgram,
+ QuestionTypeFunction,
+ QuestionTypeParsons,
+ QuestionTypeDebugging,
+)
+
+from codewof.tests.codewof_test_data_generator import (
+ generate_users,
+ generate_badges,
+ generate_questions,
+ generate_attempts,
+)
+from codewof.tests.conftest import user
+
+User = get_user_model()
+
+
+class ProfileModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_users(user)
+ generate_questions()
+ generate_badges()
+
+ def test_profile_starts_with_no_points(self):
+ user = User.objects.get(id=1)
+ points = user.profile.points
+ self.assertEqual(points, 0)
+
+ def test_profile_starts_with_create_account_badge(self):
+ user = User.objects.get(id=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="create-account")
+ earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(earned), 1)
+
+ def test_attempted_questions(self):
+ user = User.objects.get(id=1)
+ generate_attempts()
+ attempted_questions = Attempt.objects.filter(profile=user.profile)
+ # generate_attempts in codewof_utils will generate 5 attempts for user 1
+ self.assertEqual(len(attempted_questions), 5)
+
+ def test_profile_starts_on_easiest_goal_level(self):
+ user = User.objects.get(id=1)
+ goal = user.profile.goal
+ self.assertEqual(goal, 1)
+
+ def test_set_goal_to_4(self):
+ user = User.objects.get(id=2)
+ user.profile.goal = 4
+ user.profile.full_clean()
+ user.profile.save()
+ double_check_user = User.objects.get(id=2)
+ self.assertEqual(double_check_user.profile.goal, 4)
+
+ def test_cannot_set_goal_less_than_1(self):
+ user = User.objects.get(id=2)
+ with self.assertRaises(ValidationError):
+ user.profile.goal = 0
+ user.profile.full_clean()
+ user.profile.save()
+ double_check_user = User.objects.get(id=2)
+ self.assertEqual(double_check_user.profile.goal, 1)
+
+ def test_cannot_set_goal_greater_than_7(self):
+ user = User.objects.get(id=2)
+ with self.assertRaises(ValidationError):
+ user.profile.goal = 8
+ user.profile.full_clean()
+ user.profile.save()
+ double_check_user = User.objects.get(id=2)
+ self.assertEqual(double_check_user.profile.goal, 1)
+
+ def test_str_representation(self):
+ user = User.objects.get(id=1)
+ self.assertEqual(str(user.profile), '{} {}'.format(user.first_name, user.last_name))
+
+
+class BadgeModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_users(user)
+ generate_badges()
+
+ def test_id_name_unique(self):
+ with self.assertRaises(IntegrityError):
+ Badge.objects.create(
+ id_name='questions-solved-1',
+ display_name='second',
+ description='second'
+ )
+
+ def test_badge_tier_zero_default(self):
+ badge = Badge.objects.create(
+ id_name='badge_name',
+ display_name='Dummy Badge',
+ description='A badge for testing'
+ )
+ self.assertEqual(badge.badge_tier, 0)
+
+ def test_str_representation(self):
+ badge = Badge.objects.get(id_name='questions-solved-1')
+ self.assertEqual(str(badge), badge.display_name)
+
+ def test_parent_badge(self):
+ badge = Badge.objects.get(id_name='attempts-made-1')
+ parent_id = badge.parent.id_name
+ self.assertEqual(parent_id, 'attempts-made-5')
+
+ def test_new_user_awards_create_account(self):
+ user = User.objects.get(pk=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="create-account")
+ earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(earned), 1)
+
+ # def test_doesnt_award_twice_create_account(self):
+ # user = User.objects.get(pk=1)
+ # badge = Badge.objects.get(id_name="create-account")
+ # Earned.objects.create(profile=user.profile, badge=badge)
+ # check_badge_conditions(user)
+
+ # earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ # self.assertEqual(len(earned), 1)
+
+ # def test_adding_unknown_badge_doesnt_break(self):
+ # Badge.objects.create(id_name="notrealbadge", display_name="test", description="test")
+ # user = User.objects.get(pk=1)
+ # check_badge_conditions(user)
+
+ def test_award_solve_1_on_correct_attempt(self):
+ user = User.objects.get(pk=1)
+ question = Question.objects.create(title="Test question", question_text="Print hello world")
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=True, user_code='')
+
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="questions-solved-1")
+ earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(earned), 1)
+
+ def test_not_award_solve_1_on_incorrect_attempt(self):
+ user = User.objects.get(pk=1)
+ question = Question.objects.create(title="Test question", question_text="Print hello world")
+ Attempt.objects.create(profile=user.profile, question=question, passed_tests=False, user_code='')
+
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="questions-solved-1")
+ earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(earned), 0)
+
+# def test_no_award_consecutive_login_2(self):
+# user = User.objects.get(pk=1)
+# login_day = LoginDay.objects.create(profile=user.profile)
+# yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
+# LoginDay.objects.select_for_update().filter(pk=2).update(day=yesterday.date())
+
+# check_badge_conditions(user)
+# badge = Badge.objects.get(id_name="login-3")
+# earned = Earned.objects.filter(profile=user.profile, badge=badge)
+# self.assertEquals(len(earned), 0)
+
+# def test_award_consecutive_login_3(self):
+# user = User.objects.get(pk=1)
+# LoginDay.objects.create(profile=user.profile)
+# yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
+# LoginDay.objects.select_for_update().filter(pk=2).update(day=yesterday.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# day_before_yesterday = datetime.datetime.now() - datetime.timedelta(days=2)
+# LoginDay.objects.select_for_update().filter(pk=3).update(day=day_before_yesterday.date())
+
+# check_badge_conditions(user)
+# badge = Badge.objects.get(id_name="login-3")
+# earned = Earned.objects.filter(profile=user.profile, badge=badge)
+# self.assertEquals(len(earned), 1)
+
+# def test_award_consecutive_login_3_from_last_week(self):
+# user = User.objects.get(pk=1)
+# LoginDay.objects.create(profile=user.profile)
+# last_week3 = datetime.datetime.now() - datetime.timedelta(days=1, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=2).update(day=last_week3.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week2 = datetime.datetime.now() - datetime.timedelta(days=2, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=3).update(day=last_week2.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week1 = datetime.datetime.now() - datetime.timedelta(days=3, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=4).update(day=last_week1.date())
+
+# check_badge_conditions(user)
+# badge = Badge.objects.get(id_name="login-3")
+# earned = Earned.objects.filter(profile=user.profile, badge=badge)
+# self.assertEquals(len(earned), 1)
+
+# def test_award_consecutive_login_3_from_last_week_5(self):
+# user = User.objects.get(pk=1)
+# LoginDay.objects.create(profile=user.profile)
+# last_week3 = datetime.datetime.now() - datetime.timedelta(days=1, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=2).update(day=last_week3.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week2 = datetime.datetime.now() - datetime.timedelta(days=2, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=3).update(day=last_week2.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week1 = datetime.datetime.now() - datetime.timedelta(days=3, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=4).update(day=last_week1.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week4 = datetime.datetime.now() - datetime.timedelta(days=4, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=5).update(day=last_week4.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week5 = datetime.datetime.now() - datetime.timedelta(days=5, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=6).update(day=last_week5.date())
+
+# check_badge_conditions(user)
+# badge = Badge.objects.get(id_name="login-3")
+# earned = Earned.objects.filter(profile=user.profile, badge=badge)
+# self.assertEquals(len(earned), 1)
+
+# def test_no_award_consecutive_login_2_from_last_week(self):
+# user = User.objects.get(pk=1)
+# LoginDay.objects.create(profile=user.profile)
+# last_week3 = datetime.datetime.now() - datetime.timedelta(days=1, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=2).update(day=last_week3.date())
+
+# LoginDay.objects.create(profile=user.profile)
+# last_week2 = datetime.datetime.now() - datetime.timedelta(days=2, weeks=1)
+# LoginDay.objects.select_for_update().filter(pk=3).update(day=last_week2.date())
+
+# check_badge_conditions(user)
+# badge = Badge.objects.get(id_name="login-3")
+# earned = Earned.objects.filter(profile=user.profile, badge=badge)
+# self.assertEquals(len(earned), 0)
+
+
+class EarnedModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ generate_users(user)
+ generate_questions()
+ generate_badges()
+ generate_attempts()
+ # award badges
+
+ def test_questions_solved_1_earnt(self):
+ user = User.objects.get(id=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="questions-solved-1")
+ qs1_earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(qs1_earned), 1)
+
+ def test_create_account_earnt(self):
+ user = User.objects.get(id=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="create-account")
+ create_acc_earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(create_acc_earned), 1)
+
+ def test_attempts_made_5_earnt(self):
+ user = User.objects.get(id=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="attempts-made-5")
+ attempts_5_earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(attempts_5_earned), 1)
+
+ def test_attempts_made_1_earnt(self):
+ user = User.objects.get(id=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="attempts-made-1")
+ attempts1_earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(attempts1_earned), 1)
+
+ def test_consecutive_days_2_earnt(self):
+ user = User.objects.get(id=1)
+ check_badge_conditions(user)
+ badge = Badge.objects.get(id_name="consecutive-days-2")
+ consec_days_earned = Earned.objects.filter(profile=user.profile, badge=badge)
+ self.assertEqual(len(consec_days_earned), 1)
+
+
+class TokenModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ Token.objects.create(name='sphere', token='abc')
+
+ def test_name_unique(self):
+ with self.assertRaises(IntegrityError):
+ Token.objects.create(name='sphere', token='def')
+
+ def test_str_representation(self):
+ token = Token.objects.get(name='sphere')
+ self.assertEqual(str(token), 'sphere')
+
+
+class QuestionModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_questions()
+
+ def test_question_slug_unique(self):
+ with self.assertRaises(IntegrityError):
+ Question.objects.create(
+ slug='question-1',
+ title='duplicate',
+ question_text=''
+ )
+
+ def test_get_absolute_url(self):
+ question = Question.objects.get(slug='question-1')
+ url = question.get_absolute_url()
+ self.assertEqual('/questions/{}/'.format(question.id), url)
+
+ def test_str_representation(self):
+ question = Question.objects.get(slug='question-1')
+ self.assertEqual(str(question), question.title)
+
+ def test_instance_of_question(self):
+ question = Question.objects.get_subclass(slug='question-1')
+ self.assertTrue(isinstance(question, Question))
+
+
+class QuestionTypeProgramModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_questions()
+
+ def test_question_type_program_instance(self):
+ program_question = Question.objects.get_subclass(slug="program-question-1")
+ self.assertTrue(isinstance(program_question, QuestionTypeProgram))
+
+ def test_question_type_program_verbose_name(self):
+ program_question = Question.objects.get_subclass(slug="program-question-1")
+ self.assertEqual(program_question._meta.verbose_name, 'Program Question')
+
+ def test_question_type_program_verbose_name_plural(self):
+ program_question = Question.objects.get_subclass(slug="program-question-1")
+ self.assertEqual(program_question._meta.verbose_name_plural, 'Program Questions')
+
+ def test_str_representation(self):
+ program_question = Question.objects.get_subclass(slug="program-question-1")
+ self.assertEqual(
+ str(program_question),
+ '{}: {}'.format(program_question.QUESTION_TYPE, program_question.title)
+ )
+
+
+class QuestionTypeFunctionModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_questions()
+
+ def test_question_type_function_instance(self):
+ function_question = Question.objects.get_subclass(slug="function-question-1")
+ self.assertTrue(isinstance(function_question, QuestionTypeFunction))
+
+ def test_question_type_function_verbose_name(self):
+ function_question = Question.objects.get_subclass(slug="function-question-1")
+ self.assertEqual(function_question._meta.verbose_name, 'Function Question')
+
+ def test_question_type_function_verbose_name_plural(self):
+ function_question = Question.objects.get_subclass(slug="function-question-1")
+ self.assertEqual(function_question._meta.verbose_name_plural, 'Function Questions')
+
+ def test_str_representation(self):
+ function_question = Question.objects.get_subclass(slug="function-question-1")
+ self.assertEqual(
+ str(function_question),
+ '{}: {}'.format(function_question.QUESTION_TYPE, function_question.title)
+ )
+
+
+class QuestionTypeParsonsModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_questions()
+
+ def test_question_type_parsons_instance(self):
+ parsons_question = Question.objects.get_subclass(slug="parsons-question-1")
+ self.assertTrue(isinstance(parsons_question, QuestionTypeParsons))
+
+ def test_question_type_parsons_verbose_name(self):
+ parsons_question = Question.objects.get_subclass(slug="parsons-question-1")
+ self.assertEqual(parsons_question._meta.verbose_name, 'Parsons Problem Question')
+
+ def test_str_representation(self):
+ parsons_question = Question.objects.get_subclass(slug="parsons-question-1")
+ self.assertEqual(
+ str(parsons_question),
+ '{}: {}'.format(parsons_question.QUESTION_TYPE, parsons_question.title)
+ )
+
+ def test_lines_as_list(self):
+ parsons_question = Question.objects.get_subclass(slug="parsons-question-1")
+ lines_list = list(parsons_question.lines.split('\n'))
+ shuffled_lines = parsons_question.lines_as_list()
+ self.assertEqual(sorted(shuffled_lines), sorted(lines_list))
+
+
+class QuestionTypeDebuggingModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_questions()
+
+ def test_question_type_debugging_instance(self):
+ debugging_question = Question.objects.get_subclass(slug="debugging-question-1")
+ self.assertTrue(isinstance(debugging_question, QuestionTypeDebugging))
+
+ def test_question_type_function_verbose_name(self):
+ debugging_question = Question.objects.get_subclass(slug="debugging-question-1")
+ self.assertEqual(debugging_question._meta.verbose_name, 'Debugging Problem Question')
+
+ def test_str_representation(self):
+ debugging_question = Question.objects.get_subclass(slug="debugging-question-1")
+ self.assertEqual(
+ str(debugging_question),
+ '{}: {}'.format(debugging_question.QUESTION_TYPE, debugging_question.title)
+ )
+
+ def test_read_only_lines_top_default(self):
+ debugging_question = Question.objects.get_subclass(slug="debugging-question-1")
+ self.assertEqual(debugging_question.read_only_lines_top, 0)
+
+ def test_read_only_lines_bottom_default(self):
+ debugging_question = Question.objects.get_subclass(slug="debugging-question-1")
+ self.assertEqual(debugging_question.read_only_lines_bottom, 0)
+
+
+# class TestCaseModelTests(TestCase):
# @classmethod
# def setUpTestData(cls):
# # never modify this object in tests - read only
-# User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion')
-
-# def setUp(self):
-# # editable version
-# User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion')
-
-# def test_profile_starts_with_no_points(self):
-# user = User.objects.get(id=1)
-# points = user.profile.points
-# self.assertEquals(points, 0)
-
-# def test_profile_starts_on_easiest_goal_level(self):
-# user = User.objects.get(id=1)
-# goal = user.profile.goal
-# self.assertEquals(goal, 1)
-
-# def test_set_goal_to_4(self):
-# user = User.objects.get(id=2)
-# user.profile.goal = 4
-# user.profile.full_clean()
-# user.profile.save()
-# double_check_user = User.objects.get(id=2)
-# self.assertEquals(double_check_user.profile.goal, 4)
-
-# def test_cannot_set_goal_less_than_1(self):
-# user = User.objects.get(id=2)
-# with self.assertRaises(ValidationError):
-# user.profile.goal = 0
-# user.profile.full_clean()
-# user.profile.save()
-# double_check_user = User.objects.get(id=2)
-# self.assertEquals(double_check_user.profile.goal, 1)
-
-# def test_cannot_set_goal_greater_than_7(self):
-# user = User.objects.get(id=2)
-# with self.assertRaises(ValidationError):
-# user.profile.goal = 8
-# user.profile.full_clean()
-# user.profile.save()
-# double_check_user = User.objects.get(id=2)
-# self.assertEquals(double_check_user.profile.goal, 1)
-
-# class QuestionModelTests(TestCase):
-# @classmethod
-# def setUpTestData(cls):
-# # never modify this object in tests - read only
-# Question.objects.create(title='Test', question_text='Hello')
-
-# def setUp(self):
-# pass
-
-# def test_question_text_label(self):
-# question = Question.objects.get(id=1)
-# field_label = question._meta.get_field('question_text').verbose_name
-# self.assertEquals(field_label, 'question text')
-
-# def test_solution_label(self):
-# question = Question.objects.get(id=1)
-# field_label = question._meta.get_field('solution').verbose_name
-# self.assertEquals(field_label, 'solution')
-
-# def test_str_question_is_title(self):
-# question = Question.objects.get(id=1)
-# self.assertEquals(str(question), question.title)
-
-# class ProgrammingFunctionModelTests(TestCase):
-# @classmethod
-# def setUpTestData(cls):
-# ProgrammingFunction.objects.create(title='Hello', question_text="Hello", function_name="hello")
-
-# def test_instance_of_question(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertTrue(isinstance(question, Question))
-
-# def test_instance_of_programming(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertTrue(isinstance(question, Programming))
-
-# def test_instance_of_programmingfunction(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertTrue(isinstance(question, ProgrammingFunction))
-
-# def test_not_instance_of_buggy(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertFalse(isinstance(question, Buggy))
-
-# def test_not_instance_of_buggyfunction(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertFalse(isinstance(question, BuggyFunction))
-
-# def test_str_question_is_title(self):
-# question = Question.objects.get(id=1)
-# self.assertEquals(str(question), question.title)
-
-
-# class BuggyModelTests(TestCase):
-# @classmethod
-# def setUpTestData(cls):
-# Buggy.objects.create(title='Hello', question_text="Hello", buggy_program="hello")
-
-# def test_instance_of_question(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertTrue(isinstance(question, Question))
-
-# def test_not_instance_of_programming(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertFalse(isinstance(question, Programming))
-
-# def test_not_instance_of_programmingfunction(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertFalse(isinstance(question, ProgrammingFunction))
-
-# def test_instance_of_buggy(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertTrue(isinstance(question, Buggy))
-
-# def test_not_instance_of_buggyfunction(self):
-# question = Question.objects.get_subclass(id=1)
-# self.assertFalse(isinstance(question, BuggyFunction))
-
-# def test_str_question_is_title(self):
-# question = Question.objects.get(id=1)
-# self.assertEquals(str(question), question.title)
+# generate_questions()
diff --git a/codewof/tests/programming/test_views.py b/codewof/tests/programming/test_views.py
index f3c0bee9f..669d3feaf 100644
--- a/codewof/tests/programming/test_views.py
+++ b/codewof/tests/programming/test_views.py
@@ -1,6 +1,4 @@
-# flake8: noqa
-
-# from django.test import TestCase as DjangoTestCase
+# from django.test import TestCase
# from django.contrib.auth.models import User
# from django.contrib.auth import login
# from unittest import skip
@@ -8,182 +6,82 @@
# import time
# import datetime
-# from questions.models import *
-# from questions.views import *
-
-
-# class ProfileViewTest(DjangoTestCase):
+# from programming.models import (
+# Token,
+# Badge,
+# Question,
+# Earned,
+# Attempt,
+# QuestionTypeProgram,
+# )
+# from codewof.tests.codewof_test_data_generator import (
+# generate_users,
+# generate_badges,
+# generate_questions,
+# generate_attempts,
+# )
+# from programming.views import (
+# CreateView,
+# QuestionListView,
+# QuestionView
+# )
+# from codewof.tests.conftest import user
+
+
+# class QuestionListViewTest():
+
+
+# class ProfileViewTest(TestCase):
# @classmethod
# def setUpTestData(cls):
# # never modify this object in tests
-# User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion')
+# generate_users(user)
+
+# def setUp(self):
+# self.client = Client()
# def login_user(self):
-# login = self.client.login(username='john', password='onion')
+# login = self.client.login(email='john@uclive.ac.nz', password='onion')
# self.assertTrue(login)
# ### tests begin ###
# def test_redirect_if_not_logged_in(self):
-# resp = self.client.get('/profile/')
-# self.assertRedirects(resp, '/login/?next=/profile/')
+# resp = self.client.get('/users/dashboard/')
+# self.assertRedirects(resp, '/accounts/login/?next=/users/dashboard/')
# def test_view_url_exists(self):
# self.login_user()
-# resp = self.client.get('/profile/')
+# resp = self.client.get('/users/dashboard/')
# self.assertEqual(resp.status_code, 200)
# def test_view_uses_correct_template(self):
# self.login_user()
-# resp = self.client.get('/profile/')
+# resp = self.client.get('/users/dashboard/')
# self.assertEqual(resp.status_code, 200)
-# self.assertTemplateUsed(resp, 'registration/profile.html')
+# self.assertTemplateUsed(resp, 'users/dashboard.html')
-# class BadgeViewTest(DjangoTestCase):
+# class BadgeViewTest(TestCase):
# @classmethod
# def setUpTestData(cls):
# # never modify this object in tests
-# user = User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion')
-# LoginDay.objects.create(profile=user.profile)
-# Badge.objects.create(id_name="create-account", display_name="test", description="test")
-# Badge.objects.create(id_name="login-3", display_name="test", description="test")
-# Badge.objects.create(id_name="solve-1", display_name="test", description="test")
-
-# def test_new_user_awards_create_account(self):
-# user = User.objects.get(pk=1)
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="create-account")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 1)
-
-# def test_doesnt_award_twice_create_account(self):
-# user = User.objects.get(pk=1)
-# badge = Badge.objects.get(id_name="create-account")
-# Earned.objects.create(profile=user.profile, badge=badge)
-# check_badge_conditions(user)
-
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 1)
-
-# def test_adding_unknown_badge_doesnt_break(self):
-# Badge.objects.create(id_name="notrealbadge", display_name="test", description="test")
-# user = User.objects.get(pk=1)
-# check_badge_conditions(user)
-
-# def test_no_award_consecutive_login_2(self):
-# user = User.objects.get(pk=1)
-# login_day = LoginDay.objects.create(profile=user.profile)
-# yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
-# LoginDay.objects.select_for_update().filter(pk=2).update(day=yesterday.date())
-
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="login-3")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 0)
-
-# def test_award_consecutive_login_3(self):
-# user = User.objects.get(pk=1)
-# LoginDay.objects.create(profile=user.profile)
-# yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
-# LoginDay.objects.select_for_update().filter(pk=2).update(day=yesterday.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# day_before_yesterday = datetime.datetime.now() - datetime.timedelta(days=2)
-# LoginDay.objects.select_for_update().filter(pk=3).update(day=day_before_yesterday.date())
-
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="login-3")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 1)
-
-# def test_award_consecutive_login_3_from_last_week(self):
-# user = User.objects.get(pk=1)
-# LoginDay.objects.create(profile=user.profile)
-# last_week3 = datetime.datetime.now() - datetime.timedelta(days=1, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=2).update(day=last_week3.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week2 = datetime.datetime.now() - datetime.timedelta(days=2, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=3).update(day=last_week2.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week1 = datetime.datetime.now() - datetime.timedelta(days=3, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=4).update(day=last_week1.date())
-
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="login-3")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 1)
-
-# def test_award_consecutive_login_3_from_last_week_5(self):
-# user = User.objects.get(pk=1)
+# generate_users(user)
+# generate_badges()
# LoginDay.objects.create(profile=user.profile)
-# last_week3 = datetime.datetime.now() - datetime.timedelta(days=1, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=2).update(day=last_week3.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week2 = datetime.datetime.now() - datetime.timedelta(days=2, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=3).update(day=last_week2.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week1 = datetime.datetime.now() - datetime.timedelta(days=3, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=4).update(day=last_week1.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week4 = datetime.datetime.now() - datetime.timedelta(days=4, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=5).update(day=last_week4.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week5 = datetime.datetime.now() - datetime.timedelta(days=5, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=6).update(day=last_week5.date())
-
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="login-3")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 1)
-
-# def test_no_award_consecutive_login_2_from_last_week(self):
-# user = User.objects.get(pk=1)
-# LoginDay.objects.create(profile=user.profile)
-# last_week3 = datetime.datetime.now() - datetime.timedelta(days=1, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=2).update(day=last_week3.date())
-
-# LoginDay.objects.create(profile=user.profile)
-# last_week2 = datetime.datetime.now() - datetime.timedelta(days=2, weeks=1)
-# LoginDay.objects.select_for_update().filter(pk=3).update(day=last_week2.date())
-
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="login-3")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 0)
-
-# def test_award_solve_1_on_completed(self):
-# user = User.objects.get(pk=1)
-# question = Programming.objects.create(title="Test question", question_text="Print hello world")
-# attempt = Attempt.objects.create(profile=user.profile, question=question, passed_tests=True, is_save=False, user_code='')
-
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="solve-1")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 1)
-
-# def test_not_award_solve_1_on_attempt(self):
-# user = User.objects.get(pk=1)
-# question = Programming.objects.create(title="Test question", question_text="Print hello world")
-# attempt = Attempt.objects.create(profile=user.profile, question=question, passed_tests=False, is_save=False, user_code='')
-# check_badge_conditions(user)
-# badge = Badge.objects.get(id_name="solve-1")
-# earned = Earned.objects.filter(profile=user.profile, badge=badge)
-# self.assertEquals(len(earned), 0)
# class BuggyQuestionViewTest(DjangoTestCase):
# @classmethod
# def setUpTestData(cls):
# # never modify this object in tests
# User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion')
-# question = Buggy.objects.create(title="test", question_text="Print input", solution="i=input()\nprint(i)", buggy_program="i=input()\nprint(i[1:])")
+# question = Buggy.objects.create(
+# title="test",
+# question_text="Print input",
+# solution="i=input()\nprint(i)",
+# buggy_program="i=input()\nprint(i[1:])"
+# )
# token_file = open("../../token_file.txt", "r")
# sphere_token = token_file.read().strip()
@@ -233,7 +131,12 @@
# self.get_the_output(user_code, buggy_stdin, exp_print, exp_return, 1, '"correct": [true]')
# def test_buggy_function(self):
-# BuggyFunction.objects.create(title="test", question_text="Print input", function_name="hi", solution="def hi(n):\n return n", buggy_program="def hi(n):\n return n+1")
+# BuggyFunction.objects.create(
+# title="test",
+# question_text="Print input",
+# function_name="hi", solution="def hi(n):\n return n",
+# buggy_program="def hi(n):\n return n+1"
+# )
# user_code = '3'
# buggy_stdin = ''
@@ -243,7 +146,13 @@
# self.get_the_output(user_code, buggy_stdin, exp_print, exp_return, 2, '"correct": [true]')
# def test_buggy_function_and_input_output(self):
-# BuggyFunction.objects.create(title="test", question_text="Print input", function_name="hi", solution="def hi(n):\n i=input()\n print(i)\n return n", buggy_program="def hi(n):\n i=input()\n print(i[1:])\n return n")
+# BuggyFunction.objects.create(
+# title="test",
+# question_text="Print input",
+# function_name="hi",
+# solution="def hi(n):\n i=input()\n print(i)\n return n",
+# buggy_program="def hi(n):\n i=input()\n print(i[1:])\n return n"
+# )
# user_code = '3'
# buggy_stdin = 'hello'
@@ -349,7 +258,8 @@
# self.get_the_output(user_code, 2, '"correct": [true]')
# def test_get_output_program_escaped_newline_not_replaced(self):
-# question = Programming.objects.create(title="Test 2", question_text="Print hello world on different lines using single print")
+# question = Programming.objects.create(
+# title="Test 2", question_text="Print hello world on different lines using single print")
# TestCaseProgram.objects.create(question=question, expected_output="hello\nworld\n")
# user_code = 'print("hello\\nworld")'
@@ -358,29 +268,36 @@
# # functions
# def test_get_output_function(self):
-# question = ProgrammingFunction.objects.create(title="Test 2", question_text="Return given word", function_name="direct_return")
+# question = ProgrammingFunction.objects.create(
+# title="Test 2", question_text="Return given word", function_name="direct_return")
# TestCaseFunction.objects.create(question=question, function_params="'hello'", expected_return="'hello'")
# user_code = 'def direct_return(word):\n return word'
# self.get_the_output(user_code, 2, '"correct": [true]')
# def test_get_output_print_function(self):
-# question = ProgrammingFunction.objects.create(title="Test 2", question_text="Print given word", function_name="direct_print")
-# TestCaseFunction.objects.create(question=question, function_params="'hello'", expected_output="hello\n", expected_return="")
+# question = ProgrammingFunction.objects.create(
+# title="Test 2", question_text="Print given word", function_name="direct_print")
+# TestCaseFunction.objects.create(
+# question=question, function_params="'hello'", expected_output="hello\n", expected_return="")
# user_code = 'def direct_print(word):\n print(word)'
# self.get_the_output(user_code, 2, '"correct": [true]')
# def test_get_output_print_and_return_function_multiple_test_cases(self):
-# question = ProgrammingFunction.objects.create(title="Test 2", question_text="Print and return given word", function_name="print_return")
-# TestCaseFunction.objects.create(question=question, function_params="'hello'", expected_output="hello\n", expected_return="'hello'")
-# TestCaseFunction.objects.create(question=question, function_params="'world'", expected_output="world\n", expected_return="'world'")
+# question = ProgrammingFunction.objects.create(
+# title="Test 2", question_text="Print and return given word", function_name="print_return")
+# TestCaseFunction.objects.create(
+# question=question, function_params="'hello'", expected_output="hello\n", expected_return="'hello'")
+# TestCaseFunction.objects.create(
+# question=question, function_params="'world'", expected_output="world\n", expected_return="'world'")
# user_code = 'def print_return(word):\n print(word)\n return word'
# self.get_the_output(user_code, 2, '"correct": [true, true]')
# def test_blank_test_function_multiple_test_cases(self):
-# question = ProgrammingFunction.objects.create(title="Test 2", question_text="Return the string doubled", function_name="return_double")
+# question = ProgrammingFunction.objects.create(
+# title="Test 2", question_text="Return the string doubled", function_name="return_double")
# TestCaseFunction.objects.create(question=question, function_params="'hello'", expected_return="'hellohello'")
# TestCaseFunction.objects.create(question=question, function_params="''", expected_return="''")
@@ -388,15 +305,19 @@
# self.get_the_output(user_code, 2, '"correct": [true, true]')
# def test_function_multiple_params(self):
-# question = ProgrammingFunction.objects.create(title="Test 2", question_text="Add the strings", function_name="add_words")
-# TestCaseFunction.objects.create(question=question, function_params="'good','night'", expected_return="'goodnight'")
+# question = ProgrammingFunction.objects.create(
+# title="Test 2", question_text="Add the strings", function_name="add_words")
+# TestCaseFunction.objects.create(
+# question=question, function_params="'good','night'", expected_return="'goodnight'")
# user_code = 'def add_words(word1, word2):\n return word1 + word2'
# self.get_the_output(user_code, 2, '"correct": [true]')
# def test_function_false_for_incorrect_answer(self):
-# question = ProgrammingFunction.objects.create(title="Test 2", question_text="Add the strings", function_name="add_words")
-# TestCaseFunction.objects.create(question=question, function_params="'good','night'", expected_return="'goodnight'")
+# question = ProgrammingFunction.objects.create(
+# title="Test 2", question_text="Add the strings", function_name="add_words")
+# TestCaseFunction.objects.create(
+# question=question, function_params="'good','night'", expected_return="'goodnight'")
# user_code = 'def add_words(word1, word2):\n return word1'
# self.get_the_output(user_code, 2, '"correct": [false]')
diff --git a/codewof/tests/users/test_models.py b/codewof/tests/users/test_models.py
index 7bc96f640..cebe3a0ba 100644
--- a/codewof/tests/users/test_models.py
+++ b/codewof/tests/users/test_models.py
@@ -1,8 +1,44 @@
import pytest
-from django.conf import settings
+from django.test import TestCase
+from users.models import UserType, User
+
+from codewof.tests.codewof_test_data_generator import generate_users
+from codewof.tests.conftest import user
pytestmark = pytest.mark.django_db
-def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL):
- assert user.get_absolute_url() == f"/users/dashboard/"
+class UserModelTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ # never modify this object in tests - read only
+ generate_users(user)
+
+ def test_default_username(self):
+ user = User.objects.create(
+ id=3,
+ first_name='Test',
+ last_name='Case',
+ email='testcase@email.com',
+ password='password',
+ user_type=UserType.objects.get(slug='other')
+ )
+ self.assertEqual(user.username, 'user' + str(user.id))
+
+ def test_user_get_absolute_url(self):
+ user = User.objects.get(id=1)
+ self.assertEqual(user.get_absolute_url(), '/users/dashboard/')
+
+ def test_str_representation(self):
+ user = User.objects.get(id=1)
+ self.assertEqual(
+ str(user),
+ '{} {} ({})'.format(user.first_name, user.last_name, user.email)
+ )
+
+ def test_full_name_representation(self):
+ user = User.objects.get(id=1)
+ self.assertEqual(
+ user.full_name(),
+ '{} {}'.format(user.first_name, user.last_name)
+ )
diff --git a/codewof/users/urls.py b/codewof/users/urls.py
index 2feb0127d..8ab747b34 100644
--- a/codewof/users/urls.py
+++ b/codewof/users/urls.py
@@ -9,4 +9,5 @@
path("dashboard/", view=views.UserDetailView.as_view(), name="dashboard"),
path("redirect/", view=views.UserRedirectView.as_view(), name="redirect"),
path("update/", view=views.UserUpdateView.as_view(), name="update"),
+ path("achievements/", view=views.UserAchievementsView.as_view(), name="achievements"),
]
diff --git a/codewof/users/views.py b/codewof/users/views.py
index b6fe70290..d8ea22ab7 100644
--- a/codewof/users/views.py
+++ b/codewof/users/views.py
@@ -13,12 +13,31 @@
from rest_framework.permissions import IsAdminUser
from users.serializers import UserSerializer
from programming import settings
-from programming.models import Question, Attempt
from users.forms import UserChangeForm
from research.models import StudyRegistration
+
+from programming.models import (
+ Question,
+ Attempt,
+ Badge
+)
+
+from programming.codewof_utils import check_badge_conditions, get_questions_answered_in_past_month
+
User = get_user_model()
+
logger = logging.getLogger(__name__)
+del logging
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'incremental': True,
+ 'root': {
+ 'level': 'DEBUG',
+ },
+}
class UserDetailView(LoginRequiredMixin, DetailView):
@@ -34,6 +53,7 @@ def get_object(self):
def get_context_data(self, **kwargs):
"""Get additional context data for template."""
+ user = self.request.user
context = super().get_context_data(**kwargs)
now = timezone.now()
today = now.date()
@@ -58,7 +78,7 @@ def get_context_data(self, **kwargs):
log_message = 'Questions for user {} on {} ({}):\n'.format(self.request.user, now, today)
for i, question in enumerate(questions):
log_message += '{}: {}\n'.format(i, question)
- logging.info(log_message)
+ logger.info(log_message)
# TODO: Also filter by questions added before today
questions = questions.filter(
@@ -71,7 +91,7 @@ def get_context_data(self, **kwargs):
log_message = 'Filtered questions for user {}:\n'.format(self.request.user)
for i, question in enumerate(questions):
log_message += '{}: {}\n'.format(i, question)
- logging.info(log_message)
+ logger.info(log_message)
# Randomly pick 3 based off seed of todays date
if len(questions) > 0:
@@ -94,7 +114,7 @@ def get_context_data(self, **kwargs):
log_message = 'Chosen questions for user {}:\n'.format(self.request.user)
for i, question in enumerate(todays_questions):
log_message += '{}: {}\n'.format(i, question)
- logging.info(log_message)
+ logger.info(log_message)
context['questions_to_do'] = todays_questions
context['all_complete'] = all_complete
@@ -111,6 +131,13 @@ def get_context_data(self, **kwargs):
study_group__in=study.groups.all(),
).exists()
context['studies'] = studies
+ context['codewof_profile'] = self.object.profile
+ context['goal'] = user.profile.goal
+ context['all_badges'] = Badge.objects.all()
+ questions_answered = get_questions_answered_in_past_month(user)
+ context['num_questions_answered'] = questions_answered
+ logger.warning(questions_answered)
+ check_badge_conditions(user)
return context
@@ -139,6 +166,27 @@ def get_redirect_url(self):
return reverse("users:dashboard")
+class UserAchievementsView(LoginRequiredMixin, DetailView):
+ """View for a user's achievements."""
+
+ model = User
+ context_object_name = 'user'
+ template_name = 'users/achievements.html'
+
+ def get_object(self):
+ """Get object for template."""
+ return self.request.user
+
+ def get_context_data(self, **kwargs):
+ """Get additional context data for template."""
+ user = self.request.user
+ context = super().get_context_data(**kwargs)
+ context['badges_not_earned'] = Badge.objects.all().difference(user.profile.earned_badges.all())
+ context['num_badges_earned'] = user.profile.earned_badges.all().count()
+ context['num_badges'] = Badge.objects.all().count()
+ return context
+
+
class UserAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint that allows users to be viewed."""
diff --git a/dev b/dev
index b60b4d977..17fe3cb4a 100755
--- a/dev
+++ b/dev
@@ -170,6 +170,12 @@ cmd_load_questions() {
}
defhelp load_questions "Load questions."
+# Run load_badges command
+cmd_load_badges() {
+ docker-compose exec django /docker_venv/bin/python3 ./manage.py load_badges
+}
+defhelp load_badges "Load badges."
+
cmd_createsuperuser() {
docker-compose exec django /docker_venv/bin/python3 ./manage.py createsuperuser
}
@@ -326,6 +332,11 @@ cmd_dev() {
fi
}
+cmd_backdate() {
+ docker-compose exec django /docker_venv/bin/python3 ./manage.py backdate_points_and_badges
+}
+defhelp backdate 'Re-calculates points and badges earned for all user profiles.'
+
# If no command given
if [ $# -eq 0 ]; then
echo -e "${RED}ERROR: This script requires a command!${NC}"
diff --git a/infrastructure/prod-deploy/update-content.sh b/infrastructure/prod-deploy/update-content.sh
index 9a969413e..3890788c0 100755
--- a/infrastructure/prod-deploy/update-content.sh
+++ b/infrastructure/prod-deploy/update-content.sh
@@ -16,3 +16,5 @@ source ./codewof/load-prod-envs.sh
./dev migrate
docker-compose exec django /docker_venv/bin/python3 ./manage.py load_user_types
docker-compose exec django /docker_venv/bin/python3 ./manage.py load_questions
+docker-compose exec django /docker_venv/bin/python3 ./manage.py load_badges
+docker-compose exec django /docker_venv/bin/python3 ./manage.py backdate
diff --git a/requirements/local.txt b/requirements/local.txt
index 0ca7def1c..7c450fa59 100644
--- a/requirements/local.txt
+++ b/requirements/local.txt
@@ -6,7 +6,7 @@ psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2
# Testing
# ------------------------------------------------------------------------------
mypy==0.770 # https://github.com/python/mypy
-pytest==5.4.1 # https://github.com/pytest-dev/pytest
+pytest==5.3.5 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar
# Code quality