From a204c9651e3eee4f8597d110c6b378dc1cefe8b7 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Wed, 3 Jul 2019 19:13:41 +1200 Subject: [PATCH 001/205] Implemented points system with deductions for incomplete attempts --- .../0002_badge_earned_loginday_token.py | 48 +++++ codewof/codewof/models.py | 47 ++--- codewof/codewof/urls.py | 1 + codewof/codewof/views.py | 175 ++++++++++++++++++ codewof/config/settings/base.py | 1 - .../general/management/commands/sampledata.py | 52 ++++++ codewof/templates/users/user_detail.html | 12 ++ dev | 2 +- 8 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 codewof/codewof/migrations/0002_badge_earned_loginday_token.py diff --git a/codewof/codewof/migrations/0002_badge_earned_loginday_token.py b/codewof/codewof/migrations/0002_badge_earned_loginday_token.py new file mode 100644 index 000000000..47caf112b --- /dev/null +++ b/codewof/codewof/migrations/0002_badge_earned_loginday_token.py @@ -0,0 +1,48 @@ +# Generated by Django 2.1.5 on 2019-07-02 09:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('codewof', '0001_initial'), + ] + + 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(auto_now_add=True)), + ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='codewof.Badge')), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='codewof.Profile')), + ], + ), + migrations.CreateModel( + name='LoginDay', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('day', models.DateField(auto_now_add=True)), + ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='codewof.Profile')), + ], + ), + migrations.CreateModel( + name='Token', + fields=[ + ('name', models.CharField(max_length=100, primary_key=True, serialize=False)), + ('token', models.CharField(max_length=500)), + ], + ), + ] diff --git a/codewof/codewof/models.py b/codewof/codewof/models.py index 79bfdcc49..94c88b70a 100644 --- a/codewof/codewof/models.py +++ b/codewof/codewof/models.py @@ -47,37 +47,40 @@ 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 LoginDay(models.Model): + profile = models.ForeignKey('Profile', on_delete=models.CASCADE) + day = models.DateField(auto_now_add=True) -# def __str__(self): -# return str(self.day) + def __str__(self): + return str(self.day) -# 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) +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) + icon_name = models.CharField(null=True, max_length=SMALL) -# def __str__(self): -# return self.display_name + def __str__(self): + return self.display_name -# 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 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) -# def __str__(self): -# return str(self.date) + def __str__(self): + return str(self.date) -# class Token(models.Model): -# name = models.CharField(max_length=SMALL, primary_key=True) -# token = models.CharField(max_length=LARGE) -# def __str__(self): -# return self.name +class Token(models.Model): + name = models.CharField(max_length=SMALL, primary_key=True) + token = models.CharField(max_length=LARGE) + + def __str__(self): + return self.name + class Attempt(models.Model): diff --git a/codewof/codewof/urls.py b/codewof/codewof/urls.py index 5f0a125df..6eefad2d6 100644 --- a/codewof/codewof/urls.py +++ b/codewof/codewof/urls.py @@ -7,6 +7,7 @@ app_name = 'codewof' urlpatterns = [ path('', views.IndexView.as_view(), name="home"), + path('users/profile/', views.ProfileView.as_view(), name="profile"), path('questions/', views.QuestionListView.as_view(), name="question_list"), path('questions//', views.QuestionView.as_view(), name="question"), path('ajax/save_question_attempt/', views.save_question_attempt, name='save_question_attempt'), diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index 142ca677c..8794adf61 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -1,10 +1,12 @@ """Views for codeWOF application.""" +import datetime from django.views import generic from django.http import JsonResponse, Http404 from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import ObjectDoesNotExist import json +import logging from codewof.models import ( Profile, @@ -12,8 +14,23 @@ TestCase, Attempt, TestCaseAttempt, + Badge, + Earned ) +logger = logging.getLogger(__name__) +del logging + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'incremental': True, + 'root': { + 'level': 'DEBUG', + }, +} + + QUESTION_JAVASCRIPT = 'js/question_types/{}.js' @@ -74,11 +91,160 @@ def save_question_attempt(request): passed=test_case_data['passed'], ) + add_points(question, profile, attempt) + result['success'] = True return JsonResponse(result) +def add_points(question, profile, attempt): + """add appropriate number of points (if any) to user's account""" + max_points_from_attempts = 3 + points_for_correct = 10 + + num_attempts = len(Attempt.objects.filter(question=question, profile=profile)) + previous_corrects = Attempt.objects.filter(question=question, profile=profile, passed_tests=True) + is_first_correct = len(previous_corrects) == 1 + + points_to_add = 0 + + if attempt.passed_tests and is_first_correct: + attempt_deductions = 0 if num_attempts == 1 else num_attempts * 2 if num_attempts < max_points_from_attempts \ + else max_points_from_attempts * 2 + points_to_add = points_for_correct - attempt_deductions + else: + if num_attempts <= max_points_from_attempts: + points_to_add += 1 + + profile.points += points_to_add + profile.full_clean() + profile.save() + + +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_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: + logger.warning("No such badge: create-account") + pass + + 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() + except Badge.DoesNotExist: + logger.warning("No such badges: questions-solved") + pass + + 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]) + logger.warning(attempt_badge.id_name) + logger.warning(num_questions) + logger.warning(len(attempted)) + if len(attempted) >= num_questions: + logger.warning("making badge") + new_achievement = Earned(profile=user.profile, badge=attempt_badge) + new_achievement.full_clean() + new_achievement.save() + except Badge.DoesNotExist: + logger.warning("No such badges: questions-solved") + 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() + + +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.""" @@ -90,6 +256,15 @@ class ProfileView(LoginRequiredMixin, generic.DetailView): def get_context_data(self, **kwargs): """Get additional context data for template.""" context = super().get_context_data(**kwargs) + + user = self.request.user + + check_badge_conditions(user) + + context['goal'] = user.profile.goal + context['all_badges'] = Badge.objects.all() + logger.warning(len(Badge.objects.all())) + context['past_5_weeks'] = get_past_5_weeks(user) return context diff --git a/codewof/config/settings/base.py b/codewof/config/settings/base.py index 525754439..5fcd44b39 100644 --- a/codewof/config/settings/base.py +++ b/codewof/config/settings/base.py @@ -3,7 +3,6 @@ import os.path import environ - # codewof/codewof/config/settings/base.py - 3 = codewof/codewof/ ROOT_DIR = environ.Path(__file__) - 3 diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 648e27451..3b2138673 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -5,6 +5,8 @@ from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress +from codewof.models import Badge + LOG_HEADER = '\n{}\n' + ('-' * 20) @@ -62,3 +64,53 @@ def handle(self, *args, **options): # Codewof management.call_command('load_questions') print('Programming question added.') + + Badge.objects.create( + id_name='create-account', + display_name='Created an account!', + description='Created your very own account', + icon_name='img/icons/bitfit/icons8-badge-create-account-48.png' + ) + + Badge.objects.create( + id_name='questions-solved-1', + display_name='Solved one question!', + description='Solved your very first question', + icon_name='img/icons/bitfit/icons8-question-solved-black-50.png' + ) + + Badge.objects.create( + id_name='questions-solved-3', + display_name='Solved three questions!', + description='Solved three questions', + icon_name='img/icons/bitfit/icons8-question-solved-bronze-50.png' + ) + + Badge.objects.create( + id_name='attempts-made-1', + display_name='Made your first attempt at a question!', + description='Attempted one question', + icon_name='img/icons/bitfit/icons8-attempt-made-black-50.png' + ) + + Badge.objects.create( + id_name='attempts-made-10', + display_name='Made 10 question attempts!', + description='Attempted ten questions', + icon_name='img/icons/bitfit/icons8-attempt-made-bronze-50.png' + ) + + Badge.objects.create( + id_name='attempts-made-100', + display_name='Made 100 question attempts!', + description='Attempted one hundred questions', + icon_name='img/icons/bitfit/icons8-attempt-made-silver-50.png' + ) + + Badge.objects.create( + id_name='attempts-made-1000', + display_name='Made 1000 question attempts!', + description='Attempted one thousand questions', + icon_name='img/icons/bitfit/icons8-attempt-made-gold-50.png' + ) + print("Badges added.") diff --git a/codewof/templates/users/user_detail.html b/codewof/templates/users/user_detail.html index f9a61ffbe..14941231c 100644 --- a/codewof/templates/users/user_detail.html +++ b/codewof/templates/users/user_detail.html @@ -11,6 +11,18 @@

{{ 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 %} diff --git a/dev b/dev index 73f1daa3b..96c7f1ebb 100755 --- a/dev +++ b/dev @@ -82,7 +82,7 @@ cmd_update() { cmd_collect_static echo "" echo -e "\n${GREEN}Content is loaded!${NC}" - echo "Open your preferred web browser to the URL 'localhost:82'" + echo "Open your preferred web browser to the URL 'localhost:83'" } defhelp update 'Update system ready for use.' From bb50db913d37522f4bb02d28431f96893dc66dc6 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Tue, 16 Jul 2019 22:44:51 +1200 Subject: [PATCH 002/205] Fixed unit tests for profile. --- .../management/commands/_BadgesLoader.py | 0 .../migrations/0003_profile_earned_badges.py | 18 ++ codewof/codewof/models.py | 2 +- codewof/codewof/views.py | 9 +- .../general/management/commands/sampledata.py | 14 +- .../badges/icons8-attempt-made-black-50.png | Bin 0 -> 1288 bytes .../badges/icons8-attempt-made-bronze-50.png | Bin 0 -> 1522 bytes .../badges/icons8-attempt-made-gold-50.png | Bin 0 -> 1462 bytes .../badges/icons8-attempt-made-silver-50.png | Bin 0 -> 1469 bytes .../badges/icons8-badge-create-account-48.png | Bin 0 -> 791 bytes .../icons8-question-solved-black-50.png | Bin 0 -> 1011 bytes .../icons8-question-solved-bronze-50.png | Bin 0 -> 1678 bytes .../badges/icons8-question-solved-gold-50.png | Bin 0 -> 1607 bytes .../icons8-question-solved-silver-50.png | Bin 0 -> 1583 bytes codewof/tests/codewof/test_models.py | 213 +++++++++--------- codewof/tests/codewof/test_views.py | 76 +++---- 16 files changed, 176 insertions(+), 156 deletions(-) create mode 100644 codewof/codewof/management/commands/_BadgesLoader.py create mode 100644 codewof/codewof/migrations/0003_profile_earned_badges.py create mode 100644 codewof/static/img/icons/badges/icons8-attempt-made-black-50.png create mode 100644 codewof/static/img/icons/badges/icons8-attempt-made-bronze-50.png create mode 100644 codewof/static/img/icons/badges/icons8-attempt-made-gold-50.png create mode 100644 codewof/static/img/icons/badges/icons8-attempt-made-silver-50.png create mode 100644 codewof/static/img/icons/badges/icons8-badge-create-account-48.png create mode 100644 codewof/static/img/icons/badges/icons8-question-solved-black-50.png create mode 100644 codewof/static/img/icons/badges/icons8-question-solved-bronze-50.png create mode 100644 codewof/static/img/icons/badges/icons8-question-solved-gold-50.png create mode 100644 codewof/static/img/icons/badges/icons8-question-solved-silver-50.png diff --git a/codewof/codewof/management/commands/_BadgesLoader.py b/codewof/codewof/management/commands/_BadgesLoader.py new file mode 100644 index 000000000..e69de29bb diff --git a/codewof/codewof/migrations/0003_profile_earned_badges.py b/codewof/codewof/migrations/0003_profile_earned_badges.py new file mode 100644 index 000000000..9067a0b3a --- /dev/null +++ b/codewof/codewof/migrations/0003_profile_earned_badges.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-07-16 16:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('codewof', '0002_badge_earned_loginday_token'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='earned_badges', + field=models.ManyToManyField(through='codewof.Earned', to='codewof.Badge'), + ), + ] diff --git a/codewof/codewof/models.py b/codewof/codewof/models.py index 94c88b70a..dcadfa60b 100644 --- a/codewof/codewof/models.py +++ b/codewof/codewof/models.py @@ -24,7 +24,7 @@ class Profile(models.Model): default=1, validators=[MinValueValidator(1), MaxValueValidator(7)] ) - # earned_badges = models.ManyToManyField('Badge', through='Earned') + earned_badges = models.ManyToManyField('Badge', through='Earned') # attempted_questions = models.ManyToManyField('Question', through='Attempt') def __str__(self): diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index 8794adf61..c6df09d3e 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -157,7 +157,10 @@ def get_consecutive_sections(days_logged_in): def check_badge_conditions(user): """check badges for account creation, days logged in, and questions solved""" earned_badges = user.profile.earned_badges.all() - + logger.warning(len(Badge.objects.all())) + logger.error(len(Badge.objects.all())) + logger.info(len(Badge.objects.all())) + logger.critical(len(Badge.objects.all())) # account creation badge try: creation_badge = Badge.objects.get(id_name="create-account") @@ -258,12 +261,14 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user - check_badge_conditions(user) context['goal'] = user.profile.goal context['all_badges'] = Badge.objects.all() logger.warning(len(Badge.objects.all())) + logger.error(len(Badge.objects.all())) + logger.info(len(Badge.objects.all())) + logger.critical(len(Badge.objects.all())) context['past_5_weeks'] = get_past_5_weeks(user) return context diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 3b2138673..38d893cdf 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -69,48 +69,48 @@ def handle(self, *args, **options): id_name='create-account', display_name='Created an account!', description='Created your very own account', - icon_name='img/icons/bitfit/icons8-badge-create-account-48.png' + icon_name='img/icons/badges/icons8-badge-create-account-48.png' ) Badge.objects.create( id_name='questions-solved-1', display_name='Solved one question!', description='Solved your very first question', - icon_name='img/icons/bitfit/icons8-question-solved-black-50.png' + icon_name='img/icons/badges/icons8-question-solved-black-50.png' ) Badge.objects.create( id_name='questions-solved-3', display_name='Solved three questions!', description='Solved three questions', - icon_name='img/icons/bitfit/icons8-question-solved-bronze-50.png' + icon_name='img/icons/badges/icons8-question-solved-bronze-50.png' ) Badge.objects.create( id_name='attempts-made-1', display_name='Made your first attempt at a question!', description='Attempted one question', - icon_name='img/icons/bitfit/icons8-attempt-made-black-50.png' + icon_name='img/icons/badges/icons8-attempt-made-black-50.png' ) Badge.objects.create( id_name='attempts-made-10', display_name='Made 10 question attempts!', description='Attempted ten questions', - icon_name='img/icons/bitfit/icons8-attempt-made-bronze-50.png' + icon_name='img/icons/badges/icons8-attempt-made-bronze-50.png' ) Badge.objects.create( id_name='attempts-made-100', display_name='Made 100 question attempts!', description='Attempted one hundred questions', - icon_name='img/icons/bitfit/icons8-attempt-made-silver-50.png' + icon_name='img/icons/badges/icons8-attempt-made-silver-50.png' ) Badge.objects.create( id_name='attempts-made-1000', display_name='Made 1000 question attempts!', description='Attempted one thousand questions', - icon_name='img/icons/bitfit/icons8-attempt-made-gold-50.png' + icon_name='img/icons/badges/icons8-attempt-made-gold-50.png' ) print("Badges added.") 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 0000000000000000000000000000000000000000..074c96897ffd4ee97bc0227fdd11297c6a038902 GIT binary patch literal 1288 zcmV+j1^4=iP)9Q z;U=QmwQW^FZr(qkEP^0{3Zhlvrd7E zd+u+}J>$$BDMUp0ABb1z32>#qAm6EQI1CF53!v3%K`NDkR4Rqq+FGz$t>APzzdi|n zm|QLwDijI?fG>m3=i}1S(x>zC201@J$Hm1(?C9vgy1F`)$z)hjQGtz(jW{(mh3Dtz zjK_L^e@B%{g#dsGg#y>t*YW1&2Ji3haeI3k8yXtYKzn<8+<@HO-QnoyC{h%a_3EXr z;ICi57>#MS+YtazC=}ww#l@%N4-XG0lgSVO&|om+58`w>v9hv~7T~`@Q51G}cjL;+ z3MLYXtm9g(Rs;Yv8jV>!gM)(z04NfP@&obtd_)kL(ca#Ufk5EPu^%2D@bdB!{eFMe z{6j-S2mmOT%ku*Y;A23L3q7h z)M~ZqrFw91z#hbAvvC3e0NB^p$2jlq?rypg6h)!UX8YtJ0vR42<_ttE7BkAZv9W70S!YvwDd@1OfqxJ*W75KJ9Uz&j$bi5{U%VYBl{>B#EU`DT$pv zF`o}rRaLYewOS1piv=VSN$vqjdSr5PlC%C$C=?{AbBbtbX*3!VJFV*L>p2?>fUrlJ zo0~aHtk>&vdR#6SjE|4Q%*+e~0s&etQI7hTA9seMn0iX$n1xhq$2Tn9F0a}-qq0Qbolo6 zMv^+Gh+nMY@i@xma@G(+p%5dH2&WK;gi8zH-{-B)&Q8+P($eeoP+D5b`uwb7L+@}n zSXX3obCc_0AYvZdK?Z|?7^11EiPgJ06R`&Icsy8ER>l~jxVRXPkB_;W`)|r?r%|KP zFb2uozTsMVZDN0Xe0+7^z{&+kNXJ=<@)8!VM^>}@K zjV&!L#CI*FQi-Rhr(CxOA`&)xBoe{-`FWzdRc4fwl;Fg~1bV&RKM&$|yK#DYIxm3C z5C{Y~K0Z$L{e(4$!{NZ*-rn?1-**H>QD`!m@aX7>dyr5lgd-y(-!}PdKx1QL7z%~B z0lB)mN`L+;M5t6Mbi3X8f%yG?R4SDPgGfbnbv5zL;5U%|{{DhPq@tsv1Cz-lImpS$ zNx>(d8Hb05j6u@B>}zXl@YA%syv*F3BGTz}1+PG6)YR0FFRj$4t)CP;9uH%&81tnS yjYc6H4l^&~A5t=zgqN2WMqUc>@Bg)al>Py!vMBYhi5|QF0000(GBnMjwa1P>O&q2$0u=>Xyk3!swT9>sjD;^4!jbPy7`OYddX*A~ zcjTfhPXoD(LD!fYC$EfQaEizAmN67&rDAJQTH-<`IX{lKjN$T4D`KV8<_^F#=|R)q z(|GTNT_7fLyc6IDehSTX?5Jd}>oHEQhrx*K+P01a)HFd>o_``{Fh zs_%MmDUOJx1m{Bd>e6l0o*BST|5!06(4fQY0sxfdX~IM-pw}o+p05P}{5L)u_KZ3s z7N;KvPxr$*>nCm#0&vyj#8s0M%{Qj;KW=5}3AU7ig8S2>M>*RyDHpl@09?nsRJ);sbYW1SE$4=8E zCfA2JwD!{ZnQ-`UsG%Pm9|QxSW^4GaMJ43d9=qt8Amx0=46*I+eGcsVwj13OZZH6j zJfDk=1-dXBsx{Iz?xrypq;teYWY^MV!zbtFr=mftz_C~JvAHlU%8p7%yhBO~E^iPH zPax)^U%=@Jpsv}Bxe!pYT8)$Yi{jc-O)a-KNEfXLF7KS+^VVJm+`f66&-N4uil!1G zmoYTv;{HT>w>al#fKDZgd9H-{qUktIU_@w>=#X0c-N+C{9fW};qAi>G{;SYl_P4NH#&J#f0o`SfxR8#9*`!tQYAQx_k zpX?T=Kj#0jpb|3UN?t~Izwj9X0PjAV1D0W+RZvU3-Dm*IGLVbxa8w!DwjrIyT#)U>WWJPgR#XH)DP&h^2A*826E_s$kz5USmpzcM zpQX^1C@g~$`-`C0NI&6~au(mz6hSFgpHM>WP0ljdv@W`gCZ#-I3$;?bg9|RkaBqR&T z)2AROT}f=OQ^`;qar2Q(652ZaLGjah0bgKwub2}O;-v`A7a$ic1<6A0IDD|plGk4T zU2^q0^arXKYe(ao)EFcm&%iyKQ^r+rqOv=|@K_MMx7wX$AxH{kvd>xQb z001gCq~pN0Y&^O~BiT>P>W|}t`26P)T)t(K?3k1g=I?g@=yPCKsU9D_m`8N|6G_x! zAG7;VeX8fKh*(Mpz{Q(ZRG;d>h}BEoK1_(s9l)W6evC?Rlq^QAUL36N!-RwGo;gfN z-32pl+wPr+1tvK^j-EFoM6oUxh4kL>;I}@ONx_F)bkX7XFjMIpcZYh9Lr5}~=1 zJf=k>TGO%Z`uL8Ve_(%N0fm_Gfz(GV7-Fdq8^y3dhzbox0g(cwyjm&UcAwqt zj31U(_R*cWvs>Em|Fqe;_sspxIrrQ%_m1RtIaTfenfiMG>CznoBKP+I63OjR`C@k` zdk%!Sa76*Y>5wR%DzkK+l?h|aaebtLHb>4hD$H5!rST>}tYkB6SYqe(7j04hrwnrX zyh5!jMBULa*RCtHx>Y0zaNE3=qU@P9xV2D#j#uwiR}>bekW5(}~!K3Z<)wYj$58{O2``$H`G z(1+a$2X_o2Z&dUHXl_+0SkS?Bw@T3znVKzGJ=Y9P_EYB-iWhtM_Q&K90O2lpYfFGR ztG#@+uM;J4!~k%(J`BLL$#NV7Ku)&A^!$hyj-Bj^x+d(A%gqYKuXt!~H5QRiD)trecFH zp=WZkB}R`h^$O9}p>p8Yh}ZJRM`tb}kcK9Oy{?c*mxTJ|_pq>?i&qt9t@PsdM3PBt z))E4#IjB7+^lE2Kr|!1<4u>hM@KArc3rT_xmf9(r61@ZpEr_ubEEOF&q0iQLKZSVt zy+|kyA1bkRT_#UWiyyWSNaNKcDMwK49+g(NnsCy0>qfiEvQ2&z1s;CD%o9x^z6 zAhH>T4NX3{yiq2~N-Q`BY2Q}}WWpE|uHzy-de?{?lcqSajJF3ayFbH0~^* z@fx8bDj|kq6C`KWk0d&aD!DLkxvs!t8chZWzm8b}O8%8H^({z2%PZs$`L&-D%Bpf3_X#8mE$IR|+oZ64SCFdOkYRvWS!9?$)5?k$^q-$3Q&Nya^Z%He@s0fM^5Oc zzg;rqku#0`KR04^@tUH$3^GBa^l*Cy)R!CpCFVi@oM QivR!s07*qoM6N<$f&sa&(f|Me literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a6b21f1c5f30575129b0837be3f4fd43fd14ad2 GIT binary patch literal 1469 zcmV;u1w#6XP)V47&&lg0(KXt&51F3z4F>5xHFmlF~&q7}1-k&w0+9%<}Sb0RI4@t4<(U{4+)*SDipK{C+>Cr>C*FxCpP; z3joN>%tS>+1sWS0QB+hE5ySj#V`O`K8}04wK?C84#bOwZMl?4!hkT!~k_kIDL)<#&=H~F~)hkG) zQcO%tK&4WJlph=%psubC-@bi=Mx(*#=xDG^&|u%#*uc}LPcc6~&uanze*E}>p`jtP zx3^<FMbq-&6OfmX?;{jtNq%Rx7dg^{ zy?gf#CX)#q$00X27iO~=_wV12&`}wQHAxBK`1lw{M@LZy{cl*7MPFYZIF7@eJ9qHr z&6}8dsz>YOw+Gv_SS-Azsf#bVy_uV23c0OWFc)OqDk zs4~mS%A%3;A{L90o14q4QK?kWYPFEd<#FqyGE!Vz442DA6B3i_*RRLz5v^7WtyW7^ zAJwCE>((tAIWO@Z6gZ+d!qg3ET6Wnr;cVrr=#t(ln_8aXc{GrP~{i~2blR7Mh6EQWJ>dKy>r;>8Py zL?Xy!vWT@+I8={TUS1wnS6695VnQUZhef-oJm(y9*Qo^$t-Y zk>K9Fdx9HOtJO(20SG*Nc=F_l;09}JYX!&hA;^eYt&Z5Ckx)`nLOjT65q$P*GMSK( zkwL6aOG|^*aEJ#K}i8N$BS}KR-{2RtWza+1uO0;o%{% z{^H^S>+9=6n~RGeBVMl;U0q!`J3Awe2?PS@?(R;R8LFB6%E4-d)M*Mz1`uqFw_U+q*lB7z6Vet6zWAya&5FP(S#D^R4 zdc80jjqrFpNlTIjrBaF4uV15}pnxha^a& zK79G|C8>!C!RPa#x3`z-3L|Lb^XJd_@Zm#B5)*>UPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0<1|yK~!i%?b$c0 z6j2z5@hj%oC`J^FjiK;Eh@hph710l%jfkKk2x{xaScttg+K8x)u@xIjMa2LX=79P< zcrPq--@SKt&Y2+ihaW7vGw+$%ojGB4AY*1>C(h#$-r)NXU;ExU?7$4PCaZ7}pYgj# zZhHYM(O9P86n-E&$X>Yb2~0t)@tb>$=%C8Dj|HeD>)|yW9Y~7jSc^)t0#zLRFi&DW zDv8&8&da8o2bhHHIfu%8e$QYC+dnMjV) zgQ$TzY)5h&@}uH)IE&;wL{z*Ej?l?@hp2cR-Xb~Q5EZY(S0v}3KLLNnsCXT+TtZFW z#P^UKmw{37I-EgrnjFZsAvq4`#i_&-q0f$>OirehQD-`Qk+xwx?_~;`#3B7=8=gnAxsY872dw%9N z(Z1GPjauS*)lo7!kTUL~vd1ai;yrN`9}pc>FYIl+VXf6zyf03ppA&IgrzlI&n)t}v zHLUX&d#l&*8AS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ca2659f3fcf2d1ddf945dd913308475416b5bbd4 GIT binary patch literal 1011 zcmVjr{ z)4e@i-7}jn{GocQ=3l>G*K~DN&C(7R;YrM3E{iYl1Xi`wXQNz!9XO7~f;fgZvbM{P z)z~hGEHW_2&^AntUhe<7Ob|1SfqWdrR$Mw_?K;?i-8F@m#qKP3r%eMF<1u{If`NR@ z;!$DJXoSmgKc$T&y(O?x*^n14q0R}fLg7VL_y?-15euj3&nN9i!8 zgf&`SzAvkMT>U$A0B1$HwhU`dtI!B}{HL`q#Q!Tb0tD^CiWs61=&KyHO-7q??2?#Af_?XHf>a|!+RY0Xc+-w6w~A`xZWn8i{Yh|1^H=m+NIp6C3C z%nQr(wphg5(e-sT^X$Pt@gMR=2%DR>cnP0p<(1f2GtZ}08ck_uZ}9_GME$&wkZXMv zBa(8*`9gZC2iO$}q@ z%7#oF(SNe|a0B_`l|d!;Heh5&ly3sRC+2&nNbI2Lx5ukh9ha~#Dz6jFr0{Z4MVXQ9 z(l=W1>b9GhYZjjrRhAh!h&K~EzXaE2k$PzTe9DK{8qdL8K7LlbQf4f?SV3TwIEK4Mti6v8o{?r^K{%lwI~_c&o{2-o zJQ9Pr({Qudeq1#cr4?8soVYJ#Vj35WNn>TsSNwCq3B02J=Z9s_ZNlkd&%{r{KOvlw htiV%vRv4+)_!nwwKwVE#FJ1ru002ovPDHLkV1flz(trQ} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a40520bd2c3d52a2397660a9b67568b5ac1ec784 GIT binary patch literal 1678 zcmV;9266d`P)h+7k=J?*?X}oHSUy4P|xAaoX#bcK=>& zO{peX%GgsZv_+Nc0IQ5*9(YHQ$ETX~_tny_ijnTcwWsFeiA93mhLZ59oEOzsMRaL{ z>H0Nl4v~yx7uTPmF`2I-xBE6Q5(j`+5T3G4W=q&y0vl0~OmlLUSh_$}E(|MNG8xfz z>dKSJmdt7a4b<0f;D$wwC&d!Z9F)1hIf1hb0-+a|*W*^^2pza|I99CV zJhO58P(!>pN)r0Vp+cY$df0F>%AE*z#?*nL2jLwm!oXopEIfq%;-bfhsO~?!*+hWi z$<<>-MgBiVU|(^;Ge?HIj09nisICLHsK@}yWMDdQN;T+*f9YN$!v4sUTLvxEX*{zvFCekK33*{O`YAkhAxxSkAX)8MW>z?k}-(~7C znBH~7^|}lKSFBHFnzcEAtf@amEPb>@*x5A`a`z1t#MQDxa5RXko?N{{%i`y-ligBx za;n+BI$`BE7F_EFgjkulbB8{wB|AdvS#EhFxqoVYI$}bfufeL3|>gKW;+48P3`f(jUk!799}WPh>CQEBp(giG$)IP3I&Zfp&S{8NRjo!>xQo* z#BkQT)jvNKEOF;FJlVAeAk}Qo1id#TS1!J<@Xheb5g9j_u3r(VI!{z(VT0*f4}*s{ z(RWH|`+~5h?&+Qpd%|*LAA}MqjZbYWy#fwiTv!RW9XLKLnIa`~D`_O=IIF@6c_8%1 z3eFv3Z~@ZI_5+Gs6@@SNPff|)KuII@t=-=tYz{4WNSD<^*P}fP&z)3ewf7BGSu=h4 zy1r6v<;SVZ%TYuJr4Q|g^5YWIQ6@#BYs3=3+5-@N;+w zzvuSWyj3ZrVuDGN-6Wz>pGst?D~n1Un~O2(HytZDk43oe`b<<8it0iYSs9XJ)7H(W zy)jyluS@K-Nj`(}V9*Ri^q!>2wgGG~jVHv^`7)kD`-W4saU=YzA`GNR7zT<?@hMJ zyz4i-;_G)r@Ic}I{P@Iq!0Em^9K5%(59a0n{VQmD(qw&`fPm{m?Xuq8u4BzY*b}5* zKx8KF>bpN^r@#wcAO(#>y>$U-zL_Y};hs6)W@!uN?N_aCxPl z*D4|OnKo&%Z3r)f?eBq*x75AWZq1~_QUt!gD#lA||NLnC%oC0RnHGU2hj4B3vFw^C zxkS2U5YPsbeHd6Cl3>53?#;Hzd>kc@!Cb0?jA@vy$ZIIa_=#7MUlf_$Xj0!+L%*tj zs@b@B>RDFI{Qx+76xGiQ1MwTqe`3SVig5u@)eEe~C8-|B^`EwGP~8lA65%>fy=beiehHC9^x Y0`3JZ|6g;s5dZ)H07*qoM6N<$g73l=!T zN`fGA<0qzClUftnYSN3+CX_U3LzA{q45T}2rW|ZlQ@%!8xEv2o3ZxY@>BQ8n&&Sbyxy2_{H<`UV68LAXJ zH=68+VBK+rTcmSLre9?9LkA@+Qe@bU1APmZX419If$@moE76k-4}((Y2U!(N{JXZ(=Zv^5cCkT4oLgoEd9qWvY9KB}AxRd@muoiSD z|NZOyU%@#udL`>kRcpNq){J~|rJ>GA2vCsfO+xMf-_S~bt4jp zv++m)7|d4vUtcNsMR&W&dDV<3K#yYDRSt$vep>uO7Qii3-WR+YTg|B3U^Y&zI+u|k zUeg-J0ESNFmVtiKM16UN>USqgvcBnkLZdF%`iI>(6L(`TV~Kyvz1typ%W8JNL8@s# zxOGy_bRq6JLT0UvRGrKR^QhKnk;F_eSdD3QFl(p=rUd0;{&1D-c%|+!sGC|}%>TOr zoQg#q4kp$1PAT6JljlvXVSH*ulWn<*G08(f&$-=|69AQC3l>Q{5R1m_Xk%3qBf*JS zktwTSaRI&7SV167Tb9B71ZkmxG0DX*(AxZ*$n+z(t8yGB4sQ;&ubFlzE;zJ!-GymesitHy9f%{+0)NQnOFLoW z;$qy%^Z<+Ek{Q-Cx28rMIWNW)suyK61?u)g4o)8EOhQ+ae$%S0rIEns zA>sMNOqVA42$m$_68yC@UpmsNy`_;{!5;$lBoW8q`cl8QqSWuLknEO3T(E1V#u;}y zm7@!m2Ttk~SN~$J4hA22PWHxDhc%cb!R)HxcfgX3N*myyZ8MpG5O=?&KJPlEco+Cw z)C2^742;(SBwY*kWK<5&JMO$%bZeeKFlVqs5Z>FDtA}5g)5-)}^0U?b{{DTGGN|sh+Tp%563EMf+`OGhRF8~im z2zgt&eC-pk(8PUV0}UR`<^BC(`$sZT>i0S&{tVm@B`zuqdUARH+fia7tq-|^KO};U zOk9V~4a8r(vj4rPVUZU)H(z3G=%A zADHpWm=q(D9`Xw1!%6ahHhzmzF6K5F_B`N)4Ed)cXH&L&=x^=NZ|_Gn#q7;S>HygC zSwxSJ3GR2lS{azBUlg?UoZ;px@8JXm!OJbW7_d8A^*6NB1hjcXrou0}>p{DLF9XOp z9~h72R{6bip*Lqn00vKXdg-xXtC@9>Vu#mN9-J$!{{VzA^C+~sc0&LF002ovPDHLk FV1i3){=onM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e3d1d5e34c54f8d32b946b9bd3f65ae84c218f37 GIT binary patch literal 1583 zcmV+~2GIG5P)qYmHP% z6s#o17Z4v19|^t^V?+}qMhzxPqK1g+k0w6QKt%l^kf5<5ra-qlvrV^(f=~?sl^8&> zK$n@FIsWLb*`4idXYR~4`u&=F?>XN&U*_Ju=iXUD128x^c%>}Mj}lQ2fLQ>*%ts|j z+9Atw-~9RWkB1$T!X(o)Z7zUU0o)BhaxpWBXe%?nuBz(KVZ{zbNIIRqN|xod03HGm z@#tm(_)wPR^$Qj(_$_D-frJbX54TyCwFbb$@Gig!gPA{&Bx!@9D2GLJ@hyazDVxpq zF!KulZWImdg1toaMl2TFK}77w(X)`Dp`lqsw48_@1aQ6Y6#Ze8h&G$1xuvtS^OzTh zxux)#BB9YkRW}qm_u91-u#Wc-50F(f+{;F(L2_d2} zOza<`SXU`R*YzITw(lgOC;-hcjJ{MVHQv$DaW0)suaISV6hO0JtRs~T=VBidBn5y| z`F#G}azoR!bD-Q5KM z7#<#;1K{OJZ01Fy(I+dCah-x5P?f85u?G>-s%hFCkw_#{EEewtAiLPzP<|ky6QY%b zZQJfb3WdTi(P-4FlSDl%1hCLF&8h58RyTb6|S0U}~?WX~34iZ_Cq^osZS9D!h0L%@-#mpP)jn_RzWwY6Nwr!7M`Y-03IBpn5 z+hixsGU_H6i^UEg;9Cg^YwL*sYRmW1K>M}5U|aQ z2*9I|_V)JE%=}2OoP6ViWm&7e8vDBY(KPK704sdB=*+AG01;h=iO&<>5+k}_QItJ~CNn>vD2gsh z5a`C5nMv364FH}CKvN*1yA(y)6(pBH#UK%}qA1T3(VK!4=a_j}P!T|g|3%4UGE0f* zBLFQPU4CWerSW+DhcM!YR-VV>@h_P<3E+SS^WBzZ#lsQ-gnZ73$KwYL!?=lvK6f!; z0KNHqen~Q!Jn3R1gi!zHREbO`vzUl_0n9FS9g`$!Wh@r!Z$!L?2>}32(^{B$4G~2R h!&sk6rA{>})<0l`0!^@h(pmrj002ovPDHLkV1lg5!07-0 literal 0 HcmV?d00001 diff --git a/codewof/tests/codewof/test_models.py b/codewof/tests/codewof/test_models.py index 99568fbfd..018b95acc 100644 --- a/codewof/tests/codewof/test_models.py +++ b/codewof/tests/codewof/test_models.py @@ -1,153 +1,150 @@ -# 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): -# @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) +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 + +User = get_user_model() + +from codewof.models import Token, Badge, Profile, Question + + +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): + @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') + 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.assertEqual(points, 0) + + 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) + + +class QuestionModelTests(TestCase): + @classmethod + def setUpTestData(cls): + # never modify this object in tests - read only + Question.objects.create(title='Test', question_text='Hello') + + def test_question_text_label(self): + question = Question.objects.get(id=1) + field_label = question._meta.get_field('question_text').verbose_name + self.assertEqual(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.assertEqual(field_label, 'solution') + + def test_str_question_is_title(self): + question = Question.objects.get(id=1) + self.assertEqual(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) diff --git a/codewof/tests/codewof/test_views.py b/codewof/tests/codewof/test_views.py index f3c0bee9f..66e49f768 100644 --- a/codewof/tests/codewof/test_views.py +++ b/codewof/tests/codewof/test_views.py @@ -1,43 +1,43 @@ # flake8: noqa -# from django.test import TestCase as DjangoTestCase -# from django.contrib.auth.models import User -# from django.contrib.auth import login -# from unittest import skip -# import json -# import time -# import datetime - -# from questions.models import * -# from questions.views import * - - -# class ProfileViewTest(DjangoTestCase): -# @classmethod -# def setUpTestData(cls): -# # never modify this object in tests -# User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion') - -# def login_user(self): -# login = self.client.login(username='john', 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/') - -# def test_view_url_exists(self): -# self.login_user() -# resp = self.client.get('/profile/') -# self.assertEqual(resp.status_code, 200) - -# def test_view_uses_correct_template(self): -# self.login_user() -# resp = self.client.get('/profile/') -# self.assertEqual(resp.status_code, 200) -# self.assertTemplateUsed(resp, 'registration/profile.html') +from django.test import TestCase as DjangoTestCase +from django.contrib.auth.models import User +from django.contrib.auth import login +from unittest import skip +import json +import time +import datetime + +from codewof.models import * +from codewof.views import * + + +class ProfileViewTest(DjangoTestCase): + @classmethod + def setUpTestData(cls): + # never modify this object in tests + User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion') + + def login_user(self): + login = self.client.login(username='john', password='onion') + self.assertTrue(login) + + ### tests begin ### + + def test_redirect_if_not_logged_in(self): + resp = self.client.get('/users/profile/') + self.assertRedirects(resp, '/accounts/login/?next=/users/profile/') + + def test_view_url_exists(self): + self.login_user() + resp = self.client.get('/users/profile/') + self.assertEqual(resp.status_code, 200) + + def test_view_uses_correct_template(self): + self.login_user() + resp = self.client.get('/users/profile/') + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, 'registration/profile.html') # class BadgeViewTest(DjangoTestCase): From d338cb2f6959d95499437a03bf7cde1cadd3a2ad Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Wed, 24 Jul 2019 05:07:27 +1200 Subject: [PATCH 003/205] Got badges working on CodeWOF website. Moved ProfileView functions to codewof_utils file. --- codewof/codewof/admin.py | 3 +- codewof/codewof/codewof_utils.py | 172 ++++++++++++++++++ codewof/codewof/views.py | 162 +---------------- .../general/management/commands/sampledata.py | 38 ++-- codewof/users/views.py | 36 +++- 5 files changed, 239 insertions(+), 172 deletions(-) create mode 100644 codewof/codewof/codewof_utils.py diff --git a/codewof/codewof/admin.py b/codewof/codewof/admin.py index 1049ee9e0..2fa08dfb7 100644 --- a/codewof/codewof/admin.py +++ b/codewof/codewof/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.contrib.auth import get_user_model -from codewof.models import Attempt +from codewof.models import Attempt, Badge User = get_user_model() @@ -15,3 +15,4 @@ class AttemptAdmin(admin.ModelAdmin): admin.site.register(Attempt, AttemptAdmin) +admin.site.register(Badge) diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py new file mode 100644 index 000000000..06d06128c --- /dev/null +++ b/codewof/codewof/codewof_utils.py @@ -0,0 +1,172 @@ +import datetime +import json +import logging + +from codewof.models import ( + Profile, + Question, + TestCase, + Attempt, + TestCaseAttempt, + Badge, + Earned +) +from django.http import JsonResponse + +logger = logging.getLogger(__name__) +del logging + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'incremental': True, + 'root': { + 'level': 'DEBUG', + }, +} + + +def add_points(question, profile, attempt): + """add appropriate number of points (if any) to user's account after a question is answered""" + max_points_from_attempts = 3 + points_for_correct = 10 + + num_attempts = len(Attempt.objects.filter(question=question, profile=profile)) + previous_corrects = Attempt.objects.filter(question=question, profile=profile, passed_tests=True) + is_first_correct = len(previous_corrects) == 1 + + points_to_add = 0 + + # check if first passed + if attempt.passed_tests and is_first_correct: + # deduct one point for up to three failed attempts + attempt_deductions = (num_attempts - 1) * 2 + points_to_add = points_for_correct - attempt_deductions + else: + # add up to three points immediately for attempts + if num_attempts <= max_points_from_attempts: + points_to_add += 1 + profile.points += points_to_add + profile.full_clean() + profile.save() + + +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_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: + #create a new account creation + new_achievement = Earned(profile=user.profile, badge=creation_badge) + new_achievement.full_clean() + new_achievement.save() + 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() + 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() + except Badge.DoesNotExist: + logger.warning("No such badges: attempts-made") + 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() + + +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 diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index c6df09d3e..4e4d9648b 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -1,6 +1,4 @@ """Views for codeWOF application.""" -import datetime - from django.views import generic from django.http import JsonResponse, Http404 from django.contrib.auth.mixins import LoginRequiredMixin @@ -18,6 +16,8 @@ Earned ) +from codewof.codewof_utils import check_badge_conditions, get_past_5_weeks, add_points + logger = logging.getLogger(__name__) del logging @@ -45,7 +45,6 @@ def get_context_data(self, **kwargs): context['questions'] = Question.objects.select_subclasses() return context - def save_question_attempt(request): """Save user's attempt for a question. @@ -97,163 +96,12 @@ def save_question_attempt(request): return JsonResponse(result) - -def add_points(question, profile, attempt): - """add appropriate number of points (if any) to user's account""" - max_points_from_attempts = 3 - points_for_correct = 10 - - num_attempts = len(Attempt.objects.filter(question=question, profile=profile)) - previous_corrects = Attempt.objects.filter(question=question, profile=profile, passed_tests=True) - is_first_correct = len(previous_corrects) == 1 - - points_to_add = 0 - - if attempt.passed_tests and is_first_correct: - attempt_deductions = 0 if num_attempts == 1 else num_attempts * 2 if num_attempts < max_points_from_attempts \ - else max_points_from_attempts * 2 - points_to_add = points_for_correct - attempt_deductions - else: - if num_attempts <= max_points_from_attempts: - points_to_add += 1 - - profile.points += points_to_add - profile.full_clean() - profile.save() - - -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_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() - logger.warning(len(Badge.objects.all())) - logger.error(len(Badge.objects.all())) - logger.info(len(Badge.objects.all())) - logger.critical(len(Badge.objects.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: - logger.warning("No such badge: create-account") - pass - - 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() - except Badge.DoesNotExist: - logger.warning("No such badges: questions-solved") - pass - - 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]) - logger.warning(attempt_badge.id_name) - logger.warning(num_questions) - logger.warning(len(attempted)) - if len(attempted) >= num_questions: - logger.warning("making badge") - new_achievement = Earned(profile=user.profile, badge=attempt_badge) - new_achievement.full_clean() - new_achievement.save() - except Badge.DoesNotExist: - logger.warning("No such badges: questions-solved") - 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() - - -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' + template_name = 'users/user_detail.html' model = Profile def get_context_data(self, **kwargs): @@ -261,14 +109,14 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user - check_badge_conditions(user) - context['goal'] = user.profile.goal context['all_badges'] = Badge.objects.all() + check_badge_conditions(user) logger.warning(len(Badge.objects.all())) logger.error(len(Badge.objects.all())) logger.info(len(Badge.objects.all())) logger.critical(len(Badge.objects.all())) + context["a"] = "b" context['past_5_weeks'] = get_past_5_weeks(user) return context diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 38d893cdf..8e4cf9cce 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -80,12 +80,26 @@ def handle(self, *args, **options): ) Badge.objects.create( - id_name='questions-solved-3', - display_name='Solved three questions!', - description='Solved three questions', + 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.objects.create( + 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.objects.create( + id_name='questions-solved-100', + display_name='Solved one hundred questions!', + description='Solved one hundred questions', + icon_name='img/icons/badges/icons8-question-solved-silver-50.png' + ) + Badge.objects.create( id_name='attempts-made-1', display_name='Made your first attempt at a question!', @@ -94,23 +108,23 @@ def handle(self, *args, **options): ) Badge.objects.create( - id_name='attempts-made-10', - display_name='Made 10 question attempts!', - description='Attempted ten questions', + 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.objects.create( - id_name='attempts-made-100', - display_name='Made 100 question attempts!', - description='Attempted one hundred questions', + 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.objects.create( - id_name='attempts-made-1000', - display_name='Made 1000 question attempts!', - description='Attempted one thousand questions', + 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' ) print("Badges added.") diff --git a/codewof/users/views.py b/codewof/users/views.py index 877549fc4..0f39f5a6d 100644 --- a/codewof/users/views.py +++ b/codewof/users/views.py @@ -5,8 +5,35 @@ from django.urls import reverse from django.views.generic import DetailView, RedirectView, UpdateView + +from codewof.models import ( + Profile, + Question, + TestCase, + Attempt, + TestCaseAttempt, + Badge, + Earned +) + +from codewof.codewof_utils import check_badge_conditions, get_past_5_weeks, add_points + +import logging + 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): """View for a user's profile.""" @@ -16,13 +43,18 @@ class UserDetailView(LoginRequiredMixin, DetailView): def get_object(self): """Get object for template.""" - if self.request.user.is_authenticated: - return User.objects.get(pk=self.request.user.pk) + 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['codewof_profile'] = self.object.profile + context['goal'] = user.profile.goal + context['all_badges'] = Badge.objects.all() + check_badge_conditions(user) + context["a"] = "b" + context['past_5_weeks'] = get_past_5_weeks(user) return context From ef4c80a192326d5565304e03c6dbd91556422832 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Wed, 24 Jul 2019 12:37:39 +1200 Subject: [PATCH 004/205] Added badges for consecutive days with attempts. Implementation begun on determining number of consecutive days for attempts to award badges. --- codewof/codewof/codewof_utils.py | 72 ++++++++++++------ .../migrations/0004_auto_20190724_2251.py | 17 +++++ codewof/codewof/models.py | 2 +- codewof/codewof/views.py | 9 ++- .../general/management/commands/sampledata.py | 37 +++++++++ .../icons/badges/icons8-calendar-14-50.png | Bin 0 -> 514 bytes .../img/icons/badges/icons8-calendar-2-50.png | Bin 0 -> 519 bytes .../icons/badges/icons8-calendar-21-50.png | Bin 0 -> 613 bytes .../icons/badges/icons8-calendar-28-50.png | Bin 0 -> 817 bytes .../img/icons/badges/icons8-calendar-7-50.png | Bin 0 -> 455 bytes 10 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 codewof/codewof/migrations/0004_auto_20190724_2251.py create mode 100644 codewof/static/img/icons/badges/icons8-calendar-14-50.png create mode 100644 codewof/static/img/icons/badges/icons8-calendar-2-50.png create mode 100644 codewof/static/img/icons/badges/icons8-calendar-21-50.png create mode 100644 codewof/static/img/icons/badges/icons8-calendar-28-50.png create mode 100644 codewof/static/img/icons/badges/icons8-calendar-7-50.png diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index 06d06128c..b3634ebe0 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime import json import logging @@ -9,7 +9,8 @@ Attempt, TestCaseAttempt, Badge, - Earned + Earned, + DayWithAttempt ) from django.http import JsonResponse @@ -83,6 +84,25 @@ def get_consecutive_sections(days_logged_in): return consecutive_sections +def get_days_consecutively_answered(user): + attempts = Attempt.objects.all() + i = 0 + logger.warning(attempts) + logger.warning(len(attempts)) + + consec_days = 0 + current_date = datetime.now() + + while i < len(attempts): + attempt = attempts[i] + + logger.warning(attempt) + + i += 1 + # logger.warning(attempts) + return 10 + + def check_badge_conditions(user): """check badges for account creation, days logged in, and questions solved""" earned_badges = user.profile.earned_badges.all() @@ -130,32 +150,40 @@ def check_badge_conditions(user): # 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() + + num_consec_days = -1 + consec_badges = Badge.objects.filter(id_name__contains="consecutive-days") + for consec_badge in consec_badges: + if consec_badge not in earned_badges: + if num_consec_days == -1: + num_consec_days = get_days_consecutively_answered(user) + 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() + + # 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() 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) + # 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 + # 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) diff --git a/codewof/codewof/migrations/0004_auto_20190724_2251.py b/codewof/codewof/migrations/0004_auto_20190724_2251.py new file mode 100644 index 000000000..0ef9041b7 --- /dev/null +++ b/codewof/codewof/migrations/0004_auto_20190724_2251.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.5 on 2019-07-24 10:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('codewof', '0003_profile_earned_badges'), + ] + + operations = [ + migrations.RenameModel( + old_name='LoginDay', + new_name='DayWithAttempt', + ), + ] diff --git a/codewof/codewof/models.py b/codewof/codewof/models.py index dcadfa60b..bc777c312 100644 --- a/codewof/codewof/models.py +++ b/codewof/codewof/models.py @@ -47,7 +47,7 @@ def save_user_profile(sender, instance, **kwargs): instance.profile.save() -class LoginDay(models.Model): +class DayWithAttempt(models.Model): profile = models.ForeignKey('Profile', on_delete=models.CASCADE) day = models.DateField(auto_now_add=True) diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index 4e4d9648b..12a447f5c 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -4,6 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import ObjectDoesNotExist import json +import datetime import logging from codewof.models import ( @@ -13,7 +14,7 @@ Attempt, TestCaseAttempt, Badge, - Earned + DayWithAttempt ) from codewof.codewof_utils import check_badge_conditions, get_past_5_weeks, add_points @@ -81,6 +82,12 @@ def save_question_attempt(request): passed_tests=total_passed == total_tests, ) + # if DayWithAttempt.objects.filter(pub_date__gte=datetime.date.today()).count < 1: + # new_attempt_day = DayWithAttempt(request.user.profile) + # new_attempt_day.full_clean() + # new_attempt_day.save() + # logger.warning(new_attempt_day) + # 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) diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 8e4cf9cce..af865a9aa 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -127,4 +127,41 @@ def handle(self, *args, **options): description='Attempted one hundred questions', icon_name='img/icons/badges/icons8-attempt-made-gold-50.png' ) + + Badge.objects.create( + 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.objects.create( + 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.objects.create( + 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.objects.create( + 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.objects.create( + 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' + ) + + print("Badges added.") 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 0000000000000000000000000000000000000000..b609e7afd3dd4b75c909d250f73580a734269b57 GIT binary patch literal 514 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|^t6#+gWuI>dsK<3~FN7?-r16^re z666=m@aWyM$K^&5x6?iU20RV*dcyMWqsN;`GuACjvpfCe6aSVeN}c}q&Wigq=^a&+ zS{lPAyu!^vaL#L&3Q#y)@^o!dPn)?XAD}z5hMyd;R8XLCRXQ z6g5`V2~3;FpO_fQxX38dCG(JP|1+<zNru#d-3t=C}u0vHe{Cu5{+>mBJe*`d#3& zJEQQ!YT=jCC$@iosMWXaSR}XBHZjnH`_aRqR}POfH>T{C6wGI1x%63GF>>8AmP@|N ztra5=D7q=EN^#io_u5mz?CsYkx*yx%DEHD@S@8%98?VvI*5fO^)GmFHc3OJkhN1m4 zHDiyH8t$)~CjOUyV!u;ulJdFz_G*VSc9!3*a(9~2WmPgSwh$DJp00i_>zopr06I;S A0{{R3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..79a690d870475b0714329eb71dfe8262b00b9276 GIT binary patch literal 519 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|^tjR8I(uI>dsK;{qv&09}j1UlEM zB*-tA!Sl(pM_y0MAFm3CW!pHN(PL@Y!sAKJ-xe6%-g5VXsp^l~`ZE^Uoc)KQ>N>>! z&VRd)qx-UtuB_lo`2(lS4gpO$?djqe;&J@#wCh}l4R~Cq&P^+OzxVsRU;p`6UvBxP zsOupTl~H`4ZpX8P2{}0p*0XvtyaXPH>KOzsxKzxTWU<21f1^kb$FzALPl&!=_QE4P zpx-P_+|M1-P-=a$e$8)Cz<#*M@C2K4d%UI0Gle#OY?H|*{HqBVg13qWu zZj`8|e>q~)z_dsK<4lRrCd@RKwr6+ z1o;Iscs=!eyy{VSdDfF>0il04vUN{q^jNlZeZ-RE1z(!qeOt&sRoQ6!-3z9wLG@>U zyxlVYR%c9&MK)*up(wH1*nJ$nx|d`HU&=3Xja~pW@~fwdV~EG`x7RxT4mk+0J~niiOwPuyjiAJ{x?tBzScC*C(SuX=z)LPX5-oVzO&wJV<=RS4V?JU|KID) z8YdQA`mNLPaKX2u9hnIt`?hM@*B<8a;Cjgte6g98BhO#sru8HrzCWt(JMLI54%@Ov zC-B5s+1I{S2Y*TVaV#m~zw}z7jq6s`UN@!C7q>f%XFOuQzhnaUi;b4t=U+sIHM?}( zQl8EJ;ZQeECrf4VU3J4rbAQK*D8|44@Nxl1WL?9T+Qv&B)laJn9hH3}mQ_0u~1YKQP_S&~yg#FUkKO<3nkp4t-4eSSkWXRIdI z%HrMsEYHQ+zu)`)-tO=J z|1a1i9OV`LB*0_gS;ole>3m}TC(J&yt?b=$^JC}xS4`5ZVvkE1BW2xQf7Og*&(7Gs z@6q0OA6|9MJSI9Tn}412!}Frbe=L_8{`su-f09sPYEadQxBC5j5&>-LF1ddlRL)QI z@Y)~3*`5&9_2|qo^{p{Gy^=0z21jj_P&xncjL^E1x0#fTmI@o+n)TrT*CwT#6DMlQ zUNbDTJ<9#QTfFk#j;T`?s;z%BMgF9)x!y)e^DX*v+jn`(y?^+wfO%s>OyrECd%Vxw zzM*velEieaV$K;`#4{p7!;e03@>%AindQlUPD!Tf@?WD=mR7Oe#ASwalWkh$W?qW6 zzUTQ<=Yf!t{4_)3DFW>_@};1ONWrc>Q6-E$OdUt67!huIjXX RJ9h&V=bo;9F6*2UngG5A=z;(M literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ae1d27761ab54b2f7a9b60a80b75fc2556d7beb5 GIT binary patch literal 455 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=3?wxlRx|^t$pJnguI>dsK<1zVb44mkfDSS! z3GxeO2zdJTk>|5_k6pZ;c>MW&yzc6>a@Gm6K1MT}9ec&*_w@@`!<+LpQX+C`oDYjl z6@FA7ex?u9yV=vlF~sBe+iTbPnj8dLA1>VW?H>0xzH`6r=W4I1TBuT-)YSP&{SMDh zryH#d7Z(T3jI8_;bSq|g#Pd_nt)4xZaLzF4XU?v Date: Fri, 2 Aug 2019 16:21:23 +1200 Subject: [PATCH 005/205] Completed consecutive day badge calculations in async attempt request. --- codewof/codewof/codewof_utils.py | 36 +++++++++---------- .../migrations/0005_auto_20190801_1022.py | 31 ++++++++++++++++ .../migrations/0006_auto_20190801_1030.py | 24 +++++++++++++ .../migrations/0007_auto_20190801_1033.py | 24 +++++++++++++ codewof/codewof/models.py | 17 ++++----- codewof/codewof/views.py | 1 - .../general/management/commands/sampledata.py | 2 +- 7 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 codewof/codewof/migrations/0005_auto_20190801_1022.py create mode 100644 codewof/codewof/migrations/0006_auto_20190801_1030.py create mode 100644 codewof/codewof/migrations/0007_auto_20190801_1033.py diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index b3634ebe0..463932002 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime import json import logging @@ -10,9 +10,11 @@ TestCaseAttempt, Badge, Earned, - DayWithAttempt ) from django.http import JsonResponse +from django.conf import settings + +time_zone = settings.TIME_ZONE logger = logging.getLogger(__name__) del logging @@ -85,22 +87,20 @@ def get_consecutive_sections(days_logged_in): def get_days_consecutively_answered(user): - attempts = Attempt.objects.all() + # get datetimes from attempts in date form) + attempts = Attempt.objects.filter(profile=user.profile).datetimes('datetime', 'day', 'DESC') + # get current day as date i = 0 - logger.warning(attempts) - logger.warning(len(attempts)) - - consec_days = 0 - current_date = datetime.now() + today = datetime.datetime.now().replace(tzinfo=None).date() while i < len(attempts): attempt = attempts[i] - - logger.warning(attempt) - + expected_date = today - datetime.timedelta(days=i) + if attempt.date() != expected_date: + break i += 1 - # logger.warning(attempts) - return 10 + + return i def check_badge_conditions(user): @@ -110,7 +110,7 @@ def check_badge_conditions(user): try: creation_badge = Badge.objects.get(id_name="create-account") if creation_badge not in earned_badges: - #create a new account creation + # create a new account creation new_achievement = Earned(profile=user.profile, badge=creation_badge) new_achievement.full_clean() new_achievement.save() @@ -118,7 +118,7 @@ def check_badge_conditions(user): logger.warning("No such badge: create-account") pass - #check questions solved badges + # 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) @@ -133,7 +133,7 @@ def check_badge_conditions(user): logger.warning("No such badges: questions-solved") pass - #checked questions attempted badges + # checked questions attempted badges try: attempt_badges = Badge.objects.filter(id_name__contains="attempts-made") attempted = Attempt.objects.filter(profile=user.profile) @@ -148,7 +148,6 @@ def check_badge_conditions(user): logger.warning("No such badges: attempts-made") pass - # consecutive days logged in badges num_consec_days = -1 @@ -158,7 +157,8 @@ def check_badge_conditions(user): if num_consec_days == -1: num_consec_days = get_days_consecutively_answered(user) n_days = int(consec_badge.id_name.split("-")[2]) - if n_days <= num_consec_days: + if n_days == num_consec_days: + logger.warning("make new consec badge") new_achievement = Earned(profile=user.profile, badge=consec_badge) new_achievement.full_clean() new_achievement.save() diff --git a/codewof/codewof/migrations/0005_auto_20190801_1022.py b/codewof/codewof/migrations/0005_auto_20190801_1022.py new file mode 100644 index 000000000..f9d7ec56b --- /dev/null +++ b/codewof/codewof/migrations/0005_auto_20190801_1022.py @@ -0,0 +1,31 @@ +# Generated by Django 2.1.5 on 2019-07-31 22:22 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('codewof', '0004_auto_20190724_2251'), + ] + + operations = [ + migrations.RemoveField( + model_name='daywithattempt', + name='profile', + ), + migrations.AlterField( + model_name='attempt', + name='datetime', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='earned', + name='date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.DeleteModel( + name='DayWithAttempt', + ), + ] diff --git a/codewof/codewof/migrations/0006_auto_20190801_1030.py b/codewof/codewof/migrations/0006_auto_20190801_1030.py new file mode 100644 index 000000000..62b6f283c --- /dev/null +++ b/codewof/codewof/migrations/0006_auto_20190801_1030.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.5 on 2019-07-31 22:30 + +import codewof.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('codewof', '0005_auto_20190801_1022'), + ] + + operations = [ + migrations.AlterField( + model_name='attempt', + name='datetime', + field=models.DateTimeField(default=codewof.models.get_local_time), + ), + migrations.AlterField( + model_name='earned', + name='date', + field=models.DateTimeField(default=codewof.models.get_local_time), + ), + ] diff --git a/codewof/codewof/migrations/0007_auto_20190801_1033.py b/codewof/codewof/migrations/0007_auto_20190801_1033.py new file mode 100644 index 000000000..ccbf46390 --- /dev/null +++ b/codewof/codewof/migrations/0007_auto_20190801_1033.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.5 on 2019-07-31 22:33 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('codewof', '0006_auto_20190801_1030'), + ] + + operations = [ + migrations.AlterField( + model_name='attempt', + name='datetime', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='earned', + name='date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/codewof/codewof/models.py b/codewof/codewof/models.py index bc777c312..832b9b39e 100644 --- a/codewof/codewof/models.py +++ b/codewof/codewof/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 @@ -15,6 +16,10 @@ User = get_user_model() +def get_local_time(): + return timezone.localtime(timezone.now()) + + class Profile(models.Model): """Profile of a user.""" @@ -47,14 +52,6 @@ def save_user_profile(sender, instance, **kwargs): instance.profile.save() -class DayWithAttempt(models.Model): - profile = models.ForeignKey('Profile', on_delete=models.CASCADE) - day = models.DateField(auto_now_add=True) - - def __str__(self): - return str(self.day) - - class Badge(models.Model): id_name = models.CharField(max_length=SMALL, unique=True) display_name = models.CharField(max_length=SMALL) @@ -68,7 +65,7 @@ def __str__(self): 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) + date = models.DateTimeField(default=timezone.now) def __str__(self): return str(self.date) @@ -98,7 +95,7 @@ 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) diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index 12a447f5c..ceddb7dbb 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -14,7 +14,6 @@ Attempt, TestCaseAttempt, Badge, - DayWithAttempt ) from codewof.codewof_utils import check_badge_conditions, get_past_5_weeks, add_points diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index af865a9aa..cd3c97c2f 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model from allauth.account.models import EmailAddress -from codewof.models import Badge +from codewof.models import Badge, Attempt LOG_HEADER = '\n{}\n' + ('-' * 20) From e031a04e447c8898c6d42843e07b16cdfd55db3e Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Wed, 7 Aug 2019 23:02:38 +1200 Subject: [PATCH 006/205] Added test data generator for test classes in codewof system --- codewof/codewof/codewof_utils.py | 16 ++++------ codewof/codewof/models.py | 8 ++--- codewof/codewof/views.py | 7 ++--- codewof/tests/codewof/test_data_generator.py | 32 ++++++++++++++++++++ codewof/tests/codewof/test_models.py | 9 +++--- codewof/tests/codewof/test_views.py | 20 ++++++------ 6 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 codewof/tests/codewof/test_data_generator.py diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index 463932002..ca230594f 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -106,6 +106,7 @@ def get_days_consecutively_answered(user): def check_badge_conditions(user): """check badges for account creation, days logged in, and questions solved""" earned_badges = user.profile.earned_badges.all() + new_badges = [] # account creation badge try: creation_badge = Badge.objects.get(id_name="create-account") @@ -114,6 +115,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=creation_badge) new_achievement.full_clean() new_achievement.save() + new_badges.append(new_achievement) except Badge.DoesNotExist: logger.warning("No such badge: create-account") pass @@ -129,6 +131,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=question_badge) new_achievement.full_clean() new_achievement.save() + new_badges.append(new_achievement) except Badge.DoesNotExist: logger.warning("No such badges: questions-solved") pass @@ -144,6 +147,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=attempt_badge) new_achievement.full_clean() new_achievement.save() + new_badges.append(new_achievement) except Badge.DoesNotExist: logger.warning("No such badges: attempts-made") pass @@ -162,17 +166,9 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=consec_badge) new_achievement.full_clean() new_achievement.save() + new_badges.append(new_achievement) - # 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() + return new_badges def get_past_5_weeks(user): diff --git a/codewof/codewof/models.py b/codewof/codewof/models.py index 832b9b39e..d876e9a3c 100644 --- a/codewof/codewof/models.py +++ b/codewof/codewof/models.py @@ -30,6 +30,7 @@ class Profile(models.Model): validators=[MinValueValidator(1), MaxValueValidator(7)] ) earned_badges = models.ManyToManyField('Badge', through='Earned') + # attempted_questions = models.ManyToManyField('Question', through='Attempt') def __str__(self): @@ -79,7 +80,6 @@ def __str__(self): return self.name - class Attempt(models.Model): """An user attempt for a question.""" @@ -98,6 +98,7 @@ class Attempt(models.Model): 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): @@ -145,8 +146,8 @@ def __str__(self): return self.title # class Meta: - # verbose_name = "Parsons Problem" - # verbose_name_plural = "All Questions & Parsons Problems" + # verbose_name = "Parsons Problem" + # verbose_name_plural = "All Questions & Parsons Problems" class TestCase(TranslatableModel): @@ -258,7 +259,6 @@ class Meta: verbose_name = 'Parsons Problem Question Test Case' - # ----- Buggy program question ------------------------------------------------ # class Buggy(Question): diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index ceddb7dbb..1b2762c84 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -81,11 +81,6 @@ def save_question_attempt(request): passed_tests=total_passed == total_tests, ) - # if DayWithAttempt.objects.filter(pub_date__gte=datetime.date.today()).count < 1: - # new_attempt_day = DayWithAttempt(request.user.profile) - # new_attempt_day.full_clean() - # new_attempt_day.save() - # logger.warning(new_attempt_day) # Create test case attempt objects for test_case_id, test_case_data in test_cases.items(): @@ -98,6 +93,8 @@ def save_question_attempt(request): add_points(question, profile, attempt) + + result['success'] = True return JsonResponse(result) diff --git a/codewof/tests/codewof/test_data_generator.py b/codewof/tests/codewof/test_data_generator.py new file mode 100644 index 000000000..a0bb243b5 --- /dev/null +++ b/codewof/tests/codewof/test_data_generator.py @@ -0,0 +1,32 @@ +""" +Class to generate test data required for testing codewof system + +""" + +from django.contrib.auth import get_user_model + +from codewof.models import Profile, Badge, Question, Attempt + +User = get_user_model() + + +def generate_questions(): + Question.objects.create(title='Test', question_text='Hello') + + +def generate_users(): + User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion') + User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion') + + +def generate_badges(): + Badge.objects.create(id_name='questions_solved_1', display_name='first', description='first') + Badge.objects.create(id_name="create-account", display_name="test", description="test") + Badge.objects.create(id_name="attempts-made-1", display_name="test", description="test") + Badge.objects.create(id_name="attempts-made-5", display_name="test", description="test") + Badge.objects.create(id_name="consecutive-days-2", display_name="test", description="test") + + +def generate_attempts(): + user = User.objects.get(id=1) + Attempt.objects.create() diff --git a/codewof/tests/codewof/test_models.py b/codewof/tests/codewof/test_models.py index 018b95acc..30eb9da0c 100644 --- a/codewof/tests/codewof/test_models.py +++ b/codewof/tests/codewof/test_models.py @@ -3,6 +3,8 @@ from django.db.utils import IntegrityError from django.contrib.auth import get_user_model +from .test_data_generator import * + User = get_user_model() from codewof.models import Token, Badge, Profile, Question @@ -20,7 +22,7 @@ def test_name_unique(self): class BadgeModelTests(TestCase): @classmethod def setUpTestData(cls): - Badge.objects.create(id_name='solve-40', display_name='first', description='first') + generate_badges() def test_id_name_unique(self): with self.assertRaises(IntegrityError): @@ -30,8 +32,7 @@ class ProfileModelTests(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') - User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion') + generate_users() def test_profile_starts_with_no_points(self): user = User.objects.get(id=1) @@ -74,7 +75,7 @@ class QuestionModelTests(TestCase): @classmethod def setUpTestData(cls): # never modify this object in tests - read only - Question.objects.create(title='Test', question_text='Hello') + generate_questions() def test_question_text_label(self): question = Question.objects.get(id=1) diff --git a/codewof/tests/codewof/test_views.py b/codewof/tests/codewof/test_views.py index 66e49f768..ef9ed0ae7 100644 --- a/codewof/tests/codewof/test_views.py +++ b/codewof/tests/codewof/test_views.py @@ -28,16 +28,16 @@ def test_redirect_if_not_logged_in(self): resp = self.client.get('/users/profile/') self.assertRedirects(resp, '/accounts/login/?next=/users/profile/') - def test_view_url_exists(self): - self.login_user() - resp = self.client.get('/users/profile/') - self.assertEqual(resp.status_code, 200) - - def test_view_uses_correct_template(self): - self.login_user() - resp = self.client.get('/users/profile/') - self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, 'registration/profile.html') + # def test_view_url_exists(self): + # self.login_user() + # resp = self.client.get('/users/profile/') + # self.assertEqual(resp.status_code, 200) + # + # def test_view_uses_correct_template(self): + # self.login_user() + # resp = self.client.get('/users/profile/') + # self.assertEqual(resp.status_code, 200) + # self.assertTemplateUsed(resp, 'registration/profile.html') # class BadgeViewTest(DjangoTestCase): From e87d40714c2c7dfe3924d933dd18eef490ecd883 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 8 Sep 2019 09:44:48 +1200 Subject: [PATCH 007/205] Update django-anymail from 6.1.0 to 7.0.0 --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 6a09f44da..34f82e07a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -7,7 +7,7 @@ filetype==1.0.4 # Django django-model-utils==3.2.0 -django-anymail[mailgun]==6.1.0 +django-anymail[mailgun]==7.0.0 django-mail-templated==2.6.5 django-allauth==0.39.1 django-crispy-forms==1.7.2 From 5225b60283d436d264c54cf35e52576512449fbe Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Tue, 17 Sep 2019 18:27:47 +1200 Subject: [PATCH 008/205] Ensured points system worked in merged app. Added points in dashbar by user dashboard link. Implemented backdate of points and badges. Re-engineered point allocation for questions. --- codewof/codewof/admin.py | 2 +- codewof/codewof/codewof_utils.py | 102 ++++++++---------- .../migrations/0003_profile_earned_badges.py | 18 ---- .../migrations/0004_auto_20190724_2251.py | 17 --- .../migrations/0005_auto_20190801_1022.py | 31 ------ .../migrations/0006_auto_20190801_1030.py | 24 ----- .../migrations/0007_auto_20190801_1033.py | 24 ----- codewof/codewof/views.py | 2 +- .../general/management/commands/sampledata.py | 18 +++- .../migrations/0003_auto_20190904_1810.py} | 29 ++--- codewof/programming/models.py | 1 + codewof/programming/urls.py | 1 - codewof/programming/views.py | 4 + codewof/static/img/icons/icons8-star-64.png | Bin 0 -> 1579 bytes codewof/static/js/question_types/base.js | 10 +- codewof/static/svg/icons8-points.svg | 1 + codewof/templates/base.html | 5 +- codewof/templates/users/dashboard.html | 13 ++- codewof/tests/codewof/test_data_generator.py | 2 +- codewof/tests/programming/test_models.py | 2 +- codewof/tests/programming/test_views.py | 2 +- codewof/users/views.py | 13 +-- 22 files changed, 119 insertions(+), 202 deletions(-) delete mode 100644 codewof/codewof/migrations/0003_profile_earned_badges.py delete mode 100644 codewof/codewof/migrations/0004_auto_20190724_2251.py delete mode 100644 codewof/codewof/migrations/0005_auto_20190801_1022.py delete mode 100644 codewof/codewof/migrations/0006_auto_20190801_1030.py delete mode 100644 codewof/codewof/migrations/0007_auto_20190801_1033.py rename codewof/{codewof/migrations/0002_badge_earned_loginday_token.py => programming/migrations/0003_auto_20190904_1810.py} (65%) create mode 100644 codewof/static/img/icons/icons8-star-64.png create mode 100644 codewof/static/svg/icons8-points.svg diff --git a/codewof/codewof/admin.py b/codewof/codewof/admin.py index 2fa08dfb7..abfaea3db 100644 --- a/codewof/codewof/admin.py +++ b/codewof/codewof/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.contrib.auth import get_user_model -from codewof.models import Attempt, Badge +from programming.models import Attempt, Badge User = get_user_model() diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index ca230594f..02c772c65 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -2,7 +2,7 @@ import json import logging -from codewof.models import ( +from programming.models import ( Profile, Question, TestCase, @@ -33,25 +33,21 @@ def add_points(question, profile, attempt): """add appropriate number of points (if any) to user's account after a question is answered""" max_points_from_attempts = 3 points_for_correct = 10 - - num_attempts = len(Attempt.objects.filter(question=question, profile=profile)) - previous_corrects = Attempt.objects.filter(question=question, profile=profile, passed_tests=True) - is_first_correct = len(previous_corrects) == 1 - + num_attempts = Attempt.objects.filter(question=question, profile=profile) + is_first_correct = len(Attempt.objects.filter(question=question, profile=profile, passed_tests=True)) == 1 points_to_add = 0 # check if first passed if attempt.passed_tests and is_first_correct: - # deduct one point for up to three failed attempts - attempt_deductions = (num_attempts - 1) * 2 - points_to_add = points_for_correct - attempt_deductions - else: - # add up to three points immediately for attempts - if num_attempts <= max_points_from_attempts: + points_to_add += 10 + if num_attempts == 1: + # correct first try points_to_add += 1 + profile.points += points_to_add profile.full_clean() profile.save() + return profile.points def save_goal_choice(request): @@ -69,24 +65,8 @@ def save_goal_choice(request): 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 get_days_consecutively_answered(user): + """Gets the number of consecutive days with questions attempted""" # get datetimes from attempts in date form) attempts = Attempt.objects.filter(profile=user.profile).datetimes('datetime', 'day', 'DESC') # get current day as date @@ -103,8 +83,17 @@ def get_days_consecutively_answered(user): return i +def get_days_with_solutions(user): + """Gets a list of dates with questions successfully answered.""" + today = datetime.datetime.now().replace(tzinfo=None).date() + attempts = Attempt.objects.filter(profile=user.profile, datetime__year=today.year, passed_tests=True).datetimes( + 'datetime', 'day', 'DESC') + return attempts + + def check_badge_conditions(user): - """check badges for account creation, days logged in, and questions solved""" + """check badges for account creation, consecutive days with questions answered, attempts made, points earned, + and questions solved""" earned_badges = user.profile.earned_badges.all() new_badges = [] # account creation badge @@ -153,7 +142,6 @@ def check_badge_conditions(user): pass # consecutive days logged in badges - num_consec_days = -1 consec_badges = Badge.objects.filter(id_name__contains="consecutive-days") for consec_badge in consec_badges: @@ -162,35 +150,37 @@ def check_badge_conditions(user): num_consec_days = get_days_consecutively_answered(user) n_days = int(consec_badge.id_name.split("-")[2]) if n_days == num_consec_days: - logger.warning("make new consec badge") new_achievement = Earned(profile=user.profile, badge=consec_badge) new_achievement.full_clean() new_achievement.save() new_badges.append(new_achievement) - return new_badges -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 +def backdate(): + """Performs backdate of all points and badges for each profile in the system.""" + profiles = Profile.objects.all() + for profile in profiles: + backdate_badges(profile) + backdate_points(profile) + + +def backdate_points(profile): + """Re-calculates 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 += 10 + if first_passed: + profile.points += 1 + + +def backdate_badges(profile): + """Re-checks the profile for badges earned.""" + check_badge_conditions(profile.user) diff --git a/codewof/codewof/migrations/0003_profile_earned_badges.py b/codewof/codewof/migrations/0003_profile_earned_badges.py deleted file mode 100644 index 9067a0b3a..000000000 --- a/codewof/codewof/migrations/0003_profile_earned_badges.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.5 on 2019-07-16 16:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('codewof', '0002_badge_earned_loginday_token'), - ] - - operations = [ - migrations.AddField( - model_name='profile', - name='earned_badges', - field=models.ManyToManyField(through='codewof.Earned', to='codewof.Badge'), - ), - ] diff --git a/codewof/codewof/migrations/0004_auto_20190724_2251.py b/codewof/codewof/migrations/0004_auto_20190724_2251.py deleted file mode 100644 index 0ef9041b7..000000000 --- a/codewof/codewof/migrations/0004_auto_20190724_2251.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.1.5 on 2019-07-24 10:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('codewof', '0003_profile_earned_badges'), - ] - - operations = [ - migrations.RenameModel( - old_name='LoginDay', - new_name='DayWithAttempt', - ), - ] diff --git a/codewof/codewof/migrations/0005_auto_20190801_1022.py b/codewof/codewof/migrations/0005_auto_20190801_1022.py deleted file mode 100644 index f9d7ec56b..000000000 --- a/codewof/codewof/migrations/0005_auto_20190801_1022.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.1.5 on 2019-07-31 22:22 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('codewof', '0004_auto_20190724_2251'), - ] - - operations = [ - migrations.RemoveField( - model_name='daywithattempt', - name='profile', - ), - migrations.AlterField( - model_name='attempt', - name='datetime', - field=models.DateTimeField(default=django.utils.timezone.now), - ), - migrations.AlterField( - model_name='earned', - name='date', - field=models.DateTimeField(default=django.utils.timezone.now), - ), - migrations.DeleteModel( - name='DayWithAttempt', - ), - ] diff --git a/codewof/codewof/migrations/0006_auto_20190801_1030.py b/codewof/codewof/migrations/0006_auto_20190801_1030.py deleted file mode 100644 index 62b6f283c..000000000 --- a/codewof/codewof/migrations/0006_auto_20190801_1030.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.5 on 2019-07-31 22:30 - -import codewof.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('codewof', '0005_auto_20190801_1022'), - ] - - operations = [ - migrations.AlterField( - model_name='attempt', - name='datetime', - field=models.DateTimeField(default=codewof.models.get_local_time), - ), - migrations.AlterField( - model_name='earned', - name='date', - field=models.DateTimeField(default=codewof.models.get_local_time), - ), - ] diff --git a/codewof/codewof/migrations/0007_auto_20190801_1033.py b/codewof/codewof/migrations/0007_auto_20190801_1033.py deleted file mode 100644 index ccbf46390..000000000 --- a/codewof/codewof/migrations/0007_auto_20190801_1033.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.5 on 2019-07-31 22:33 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('codewof', '0006_auto_20190801_1030'), - ] - - operations = [ - migrations.AlterField( - model_name='attempt', - name='datetime', - field=models.DateTimeField(default=django.utils.timezone.now), - ), - migrations.AlterField( - model_name='earned', - name='date', - field=models.DateTimeField(default=django.utils.timezone.now), - ), - ] diff --git a/codewof/codewof/views.py b/codewof/codewof/views.py index 1b2762c84..3942e055f 100644 --- a/codewof/codewof/views.py +++ b/codewof/codewof/views.py @@ -7,7 +7,7 @@ import datetime import logging -from codewof.models import ( +from programming.models import ( Profile, Question, TestCase, diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 754556afb..df0eeb9c7 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -11,7 +11,7 @@ StudyGroupFactory, ) -from codewof.models import Badge, Attempt +from programming.models import Badge, Attempt LOG_HEADER = '\n{}\n' + ('-' * 20) @@ -68,6 +68,22 @@ def handle(self, *args, **options): primary=True, verified=True ) + + user2 = User.objects.create_user( + 'user2', + 'user2@codewof.co.nz', + password="password", + first_name='Alex', + last_name='Doe', + user_type=UserType.objects.get(slug='student') + ) + EmailAddress.objects.create( + user=user2, + email=user2.email, + primary=True, + verified=True + ) + UserFactory.create_batch(size=100) print('Users created.') diff --git a/codewof/codewof/migrations/0002_badge_earned_loginday_token.py b/codewof/programming/migrations/0003_auto_20190904_1810.py similarity index 65% rename from codewof/codewof/migrations/0002_badge_earned_loginday_token.py rename to codewof/programming/migrations/0003_auto_20190904_1810.py index 47caf112b..d922845fe 100644 --- a/codewof/codewof/migrations/0002_badge_earned_loginday_token.py +++ b/codewof/programming/migrations/0003_auto_20190904_1810.py @@ -1,13 +1,14 @@ -# Generated by Django 2.1.5 on 2019-07-02 09:31 +# 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 = [ - ('codewof', '0001_initial'), + ('programming', '0002_auto_20190813_1548'), ] operations = [ @@ -25,17 +26,9 @@ class Migration(migrations.Migration): name='Earned', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateTimeField(auto_now_add=True)), - ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='codewof.Badge')), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='codewof.Profile')), - ], - ), - migrations.CreateModel( - name='LoginDay', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('day', models.DateField(auto_now_add=True)), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='codewof.Profile')), + ('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( @@ -45,4 +38,14 @@ class Migration(migrations.Migration): ('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/models.py b/codewof/programming/models.py index 9bb0a9a13..71629629c 100644 --- a/codewof/programming/models.py +++ b/codewof/programming/models.py @@ -62,6 +62,7 @@ class Badge(models.Model): def __str__(self): return self.display_name + class Earned(models.Model): profile = models.ForeignKey('Profile', on_delete=models.CASCADE) badge = models.ForeignKey('Badge', on_delete=models.CASCADE) diff --git a/codewof/programming/urls.py b/codewof/programming/urls.py index 244dd488b..210b5362f 100644 --- a/codewof/programming/urls.py +++ b/codewof/programming/urls.py @@ -10,7 +10,6 @@ path('questions/', views.QuestionListView.as_view(), name='question_list'), path('questions/create/', views.CreateView.as_view(), name='create'), path('questions//', views.QuestionView.as_view(), name='question'), - path('users/profile/', views.ProfileView.as_view(), name="profile"), path('ajax/save_question_attempt/', views.save_question_attempt, name='save_question_attempt'), # path('skills//', views.SkillView.as_view(), name="skill"), # path('random//', views.get_random_question, name='random'), diff --git a/codewof/programming/views.py b/codewof/programming/views.py index 6967122dc..575708dc9 100644 --- a/codewof/programming/views.py +++ b/codewof/programming/views.py @@ -17,6 +17,8 @@ ) from research.models import StudyRegistration +from codewof.codewof_utils import add_points + QUESTION_JAVASCRIPT = 'js/question_types/{}.js' @@ -177,6 +179,8 @@ def save_question_attempt(request): passed=test_case_data['passed'], ) result['success'] = True + points = add_points(question, profile, attempt) + result['curr_points'] = points else: result['success'] = False result['message'] = 'Attempt not saved, same as previous attempt.' 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 0000000000000000000000000000000000000000..2abeca3dca9347983bcc40c62603a50b7dc07d6c GIT binary patch literal 1579 zcmV+`2Gse9P)i|UK~#9!?OW?_8$}Se1PJ(69=W@wrBqM~q6h&6q!v)ApyC4`fIg6r z+VX)yg#@4A8z2=Z{{WzhB2Wb+w091vlBR(|D^eb5Q%qh6ftrVt*ghLK^&>cb+so|w zlGyRb4D-KJ)Wv3-g_P zn2>A=IE`urjP1h5wV6IdSJ*+${A4>>p!vx*^k4~;uvwSzG`_|TbbLJzkZdNor~vxA z@8m=1M}g)o)lEN_*q|T!YfrXJM65^*K>ct?iX6YgQyB{E8;}O z43g&a*t-xJKSIh8{U5#+2zx4+zjy(4_fOmhJDn)20A>X}8?lwpUt0KMH?K$E{jtwU zHHwUVR);9D1iYqUczCxAy;ma7$8~*?i=Ps`6uEd9+X{Xw zg04-Xweu@MJUemSvOI8qL&(LS-7#P{&bo-hc@~}N&yJ2(cxP^tamw(wq z!lUmqA0PLfCF#f%;YFE5h$I6eBPU6J49`m_QcZ$x{HW*yz;UH)~;s`-s&j;qM2FxJ|zm z$YNXG1<1-(HJN?P^POdck9*I+#@uTBA_N7vYCrp{NzC2baN%R>X%K#mk$Ye|FZ~rm z#x6img6l*Bh5w#eqrrl<6Z-$Wp~jj~7uS$zxy1BNz&px&+HA0g@Rs;UUQ4Bz)l|SgRYXssxDTc~cH6 zkZ|&WS^^=xoJofGg~Biv_k5rr5UL>*)|CWx-^1Mkyj>gk!ejZNFl%DsIQVSePVNe* zCC=C9xx{Dk8y;@tnGT1B-sY}=(9m0W>97_H4Dl;^wBdLFe(3Op64fBuipFPx#H@pV zws&cO;{_-kKm>etYvC~=6n1otgR3CO2JY2nlUSq>L6NFL4(C} z$gr^2>;#v8-d#;{mc{u`ZC&aU_;zzHp~_||dxD={Fpmh!!tQFNu}L!%ghvmS2QC0B zq*z|rhKW;K817e&!XxkEmc}$I*l%W%=#;(q0)tC&wr{)cF)p2Xnn7Gep1>)atOtnu z(97e=;2(<%PKEy_f&P8Q1FEyT01X~p94{FdHdP+i-CFCBg+~lENB}NCG5vI8h6EjN zvcGwF6{^J4+&CNC;FuXG`){hTOGG+kBnJpO-rxaNl!V`?QoUVw8%caVG}t614K`tW zTv0={p(d%k;3(Y0!UtVS%Q|zN;dbfqCTWo^Zj_Y8C;4=XD$>rTYn6q1=l``?I?laW zI{!HEL6GszyjPL7;Jz&ay|>scvCnS6Cy0^9A=Ln&F3xk;$*o9 zlD4AJ>Wj(vA \ 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 @@
+

Points earned: {{user.profile.points}}

Achievements

-

Coming soon!

+ +
+ {% for badge in all_badges %} + {% if badge in user.profile.earned_badges.all %} + + {{badge.display_name}} + {% else %} + {{badge.display_name}} + {% endif %} +
+ {% endfor %}
diff --git a/codewof/tests/codewof/test_data_generator.py b/codewof/tests/codewof/test_data_generator.py index a0bb243b5..1bdf7d7ac 100644 --- a/codewof/tests/codewof/test_data_generator.py +++ b/codewof/tests/codewof/test_data_generator.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model -from codewof.models import Profile, Badge, Question, Attempt +from programming.models import Profile, Badge, Question, Attempt User = get_user_model() diff --git a/codewof/tests/programming/test_models.py b/codewof/tests/programming/test_models.py index 30eb9da0c..91b0ea224 100644 --- a/codewof/tests/programming/test_models.py +++ b/codewof/tests/programming/test_models.py @@ -7,7 +7,7 @@ User = get_user_model() -from codewof.models import Token, Badge, Profile, Question +from programming.models import Token, Badge, Profile, Question class TokenModelTests(TestCase): diff --git a/codewof/tests/programming/test_views.py b/codewof/tests/programming/test_views.py index ef9ed0ae7..c149b9343 100644 --- a/codewof/tests/programming/test_views.py +++ b/codewof/tests/programming/test_views.py @@ -8,7 +8,7 @@ import time import datetime -from codewof.models import * +from programming.models import * from codewof.views import * diff --git a/codewof/users/views.py b/codewof/users/views.py index 414d80d3f..6380fc978 100644 --- a/codewof/users/views.py +++ b/codewof/users/views.py @@ -10,12 +10,11 @@ from django.core.exceptions import ObjectDoesNotExist from django.views.generic import DetailView, RedirectView, UpdateView from programming import settings -from programming.models import Question, Attempt from users.forms import UserChangeForm from research.models import StudyRegistration -from codewof.models import ( +from programming.models import ( Profile, Question, TestCase, @@ -25,7 +24,7 @@ Earned ) -from codewof.codewof_utils import check_badge_conditions, get_past_5_weeks, add_points +from codewof.codewof_utils import check_badge_conditions, add_points import logging @@ -82,7 +81,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( @@ -95,7 +94,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: @@ -118,7 +117,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 @@ -139,8 +138,6 @@ def get_context_data(self, **kwargs): context['goal'] = user.profile.goal context['all_badges'] = Badge.objects.all() check_badge_conditions(user) - context["a"] = "b" - context['past_5_weeks'] = get_past_5_weeks(user) return context From 2be31d23b9e4ac57b5532f8e84fb64b7e8ab0640 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Tue, 17 Sep 2019 19:59:14 +1200 Subject: [PATCH 009/205] Added custom backdate command. Fixed points adding per question - now adds extra point for correct on the first try correctly. --- codewof/codewof/codewof_utils.py | 19 +++++++++++++------ dev | 5 +++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index 02c772c65..55cf76f98 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -31,16 +31,16 @@ def add_points(question, profile, attempt): """add appropriate number of points (if any) to user's account after a question is answered""" - max_points_from_attempts = 3 - points_for_correct = 10 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 += 10 - if num_attempts == 1: + if len(num_attempts) == 1: # correct first try points_to_add += 1 @@ -154,15 +154,20 @@ def check_badge_conditions(user): new_achievement.full_clean() new_achievement.save() new_badges.append(new_achievement) + user.profile = backdate_points(user.profile) + # backdate_badges(user.profile) return new_badges -def backdate(): +def backdate_points_and_badges(): """Performs backdate of all points and badges for each profile in the system.""" profiles = Profile.objects.all() for profile in profiles: - backdate_badges(profile) - backdate_points(profile) + profile = backdate_badges(profile) + profile = backdate_points(profile) + # save profile when update is completed + profile.full_clean() + profile.save() def backdate_points(profile): @@ -179,8 +184,10 @@ def backdate_points(profile): profile.points += 10 if first_passed: profile.points += 1 + return profile def backdate_badges(profile): """Re-checks the profile for badges earned.""" check_badge_conditions(profile.user) + return profile diff --git a/dev b/dev index b60b4d977..97c31849f 100755 --- a/dev +++ b/dev @@ -326,6 +326,11 @@ cmd_dev() { fi } +cmd_backdate() { + docker-compose exec django /docker_venv/bin/python3 ./codewof/codewof_utils.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}" From 4011b3379d8b3d57483f502de727bb87e817f22d Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Tue, 17 Sep 2019 20:24:11 +1200 Subject: [PATCH 010/205] Added custom badge card html - not quite functional yet --- codewof/codewof/codewof_utils.py | 2 +- codewof/templates/programming/badge_card.html | 15 +++++++++++++++ codewof/templates/users/dashboard.html | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 codewof/templates/programming/badge_card.html diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index 55cf76f98..ab08f7c29 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -154,7 +154,7 @@ def check_badge_conditions(user): new_achievement.full_clean() new_achievement.save() new_badges.append(new_achievement) - user.profile = backdate_points(user.profile) + # user.profile = backdate_points(user.profile) # backdate_badges(user.profile) return new_badges 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/users/dashboard.html b/codewof/templates/users/dashboard.html index f3fcd36a3..b28e79eb4 100644 --- a/codewof/templates/users/dashboard.html +++ b/codewof/templates/users/dashboard.html @@ -44,6 +44,8 @@

Achievements

{% else %} {{badge.display_name}} {% endif %} + +
{% endfor %}
From 1b4bd1391712cfad7275bcc955b6a6b7f220dab9 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Wed, 18 Sep 2019 23:35:29 +1200 Subject: [PATCH 011/205] Added code for toasts when badges earned (toasts still don't show - bug). Added more attempts to test data generator. User test creation broken - user_type) --- codewof/__init__.py | 0 codewof/codewof/codewof_utils.py | 8 ++++---- codewof/programming/views.py | 4 +++- codewof/static/js/question_types/base.js | 13 +++++++++++-- codewof/templates/programming/question.html | 8 ++++++++ ...nerator.py => codewof_test_data_generator.py} | 16 +++++++++++++--- codewof/tests/programming/test_models.py | 11 ++++++++--- codewof/tests/programming/test_views.py | 2 +- 8 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 codewof/__init__.py rename codewof/tests/{codewof/test_data_generator.py => codewof_test_data_generator.py} (56%) diff --git a/codewof/__init__.py b/codewof/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index ab08f7c29..70474c696 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -104,7 +104,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=creation_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(new_achievement) + new_badges.append(creation_badge.display_name) except Badge.DoesNotExist: logger.warning("No such badge: create-account") pass @@ -120,7 +120,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=question_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(new_achievement) + new_badges.append(question_badge.display_name) except Badge.DoesNotExist: logger.warning("No such badges: questions-solved") pass @@ -136,7 +136,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=attempt_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(new_achievement) + new_badges.append(attempt_badge.display_name) except Badge.DoesNotExist: logger.warning("No such badges: attempts-made") pass @@ -153,7 +153,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=consec_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(new_achievement) + new_badges.append(consec_badge.display_name) # user.profile = backdate_points(user.profile) # backdate_badges(user.profile) return new_badges diff --git a/codewof/programming/views.py b/codewof/programming/views.py index 575708dc9..ebf64bb24 100644 --- a/codewof/programming/views.py +++ b/codewof/programming/views.py @@ -17,7 +17,7 @@ ) from research.models import StudyRegistration -from codewof.codewof_utils import add_points +from codewof.codewof_utils import add_points, check_badge_conditions QUESTION_JAVASCRIPT = 'js/question_types/{}.js' @@ -180,7 +180,9 @@ def save_question_attempt(request): ) result['success'] = True points = add_points(question, profile, attempt) + badges = check_badge_conditions(profile.user) result['curr_points'] = points + result['badges'] = badges else: result['success'] = False result['message'] = 'Attempt not saved, same as previous attempt.' diff --git a/codewof/static/js/question_types/base.js b/codewof/static/js/question_types/base.js index f10da6879..17d5db670 100644 --- a/codewof/static/js/question_types/base.js +++ b/codewof/static/js/question_types/base.js @@ -9,7 +9,7 @@ function ajax_request(url_name, data, success_function) { contentType: 'application/json; charset=utf-8', headers: { "X-CSRFToken": csrf_token }, dataType: 'json', - success: update_points + success: update_gamification }); } @@ -25,10 +25,19 @@ function clear_submission_feedback() { $('#submission_feedback').empty(); } -function update_points(data) { +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. + console.log("pointsed") + badges = data.badges; + console.log(badges); + $("#toast-header").innerText = "New badges!"; + $("#toast-body").innerText = badges; + $("#badge-toast").toast({ + delay: 3000 + }); + console.log("toasted"); } function display_submission_feedback(test_cases) { diff --git a/codewof/templates/programming/question.html b/codewof/templates/programming/question.html index 57ef0ff57..159892b2f 100644 --- a/codewof/templates/programming/question.html +++ b/codewof/templates/programming/question.html @@ -55,6 +55,14 @@

{{ question.title }}

+
+
+ Badge! +
+
+ badge! +
+
{% endblock %} {% block scripts %} diff --git a/codewof/tests/codewof/test_data_generator.py b/codewof/tests/codewof_test_data_generator.py similarity index 56% rename from codewof/tests/codewof/test_data_generator.py rename to codewof/tests/codewof_test_data_generator.py index 1bdf7d7ac..e1519ae10 100644 --- a/codewof/tests/codewof/test_data_generator.py +++ b/codewof/tests/codewof_test_data_generator.py @@ -4,9 +4,12 @@ """ from django.contrib.auth import get_user_model +from datetime import datetime from programming.models import Profile, Badge, Question, Attempt +from users.models import UserType + User = get_user_model() @@ -15,8 +18,9 @@ def generate_questions(): def generate_users(): - User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion') - User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion') + usertype = UserType.objects.create(slug='student') + User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion', user_type=usertype) + User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion', user_type=usertype) def generate_badges(): @@ -29,4 +33,10 @@ def generate_badges(): def generate_attempts(): user = User.objects.get(id=1) - Attempt.objects.create() + question = Question.objects.get(id=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/programming/test_models.py b/codewof/tests/programming/test_models.py index 91b0ea224..58405afe5 100644 --- a/codewof/tests/programming/test_models.py +++ b/codewof/tests/programming/test_models.py @@ -3,11 +3,11 @@ from django.db.utils import IntegrityError from django.contrib.auth import get_user_model -from .test_data_generator import * +from codewof.tests.codewof_test_data_generator import * User = get_user_model() -from programming.models import Token, Badge, Profile, Question +from programming.models import Token, Badge, Question class TokenModelTests(TestCase): @@ -19,14 +19,19 @@ def test_name_unique(self): with self.assertRaises(IntegrityError): Token.objects.create(name='sphere', token='def') + class BadgeModelTests(TestCase): @classmethod def setUpTestData(cls): + # generate_users() + # generate_questions() generate_badges() + # generate_attempts() def test_id_name_unique(self): with self.assertRaises(IntegrityError): - Badge.objects.create(id_name='solve-40', display_name='second', description='second') + Badge.objects.create(id_name='questions_solved_1', display_name='second', description='second') + class ProfileModelTests(TestCase): @classmethod diff --git a/codewof/tests/programming/test_views.py b/codewof/tests/programming/test_views.py index c149b9343..f6c841803 100644 --- a/codewof/tests/programming/test_views.py +++ b/codewof/tests/programming/test_views.py @@ -9,7 +9,7 @@ import datetime from programming.models import * -from codewof.views import * +from programming.views import * class ProfileViewTest(DjangoTestCase): From 7e093a0a8a80f5f3eaae8f8280bd3989443ddfb4 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Thu, 19 Sep 2019 15:17:40 +1200 Subject: [PATCH 012/205] Attempted to use UserFactory to generate test users, unsuccessful. Added badge_tier field to Badge model for points calculation. --- .../general/management/commands/sampledata.py | 42 ++++++++++++------- .../migrations/0004_badge_badge_tier.py | 18 ++++++++ codewof/programming/models.py | 1 + codewof/tests/codewof_test_data_generator.py | 12 ++++-- codewof/tests/programming/test_models.py | 3 +- 5 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 codewof/programming/migrations/0004_badge_badge_tier.py diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index df0eeb9c7..807e8a2c2 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -100,98 +100,112 @@ def handle(self, *args, **options): 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' + icon_name='img/icons/badges/icons8-badge-create-account-48.png', + badge_tier=1 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-question-solved-black-50.png', + badge_tier=1 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-question-solved-bronze-50.png', + badge_tier=2 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-question-solved-silver-50.png', + badge_tier=3 ) Badge.objects.create( id_name='questions-solved-100', display_name='Solved one hundred questions!', description='Solved one hundred questions', - icon_name='img/icons/badges/icons8-question-solved-silver-50.png' + icon_name='img/icons/badges/icons8-question-solved-silver-50.png', + badge_tier=4 ) Badge.objects.create( id_name='attempts-made-1', display_name='Made your first attempt at a question!', description='Attempted one question', - icon_name='img/icons/badges/icons8-attempt-made-black-50.png' + icon_name='img/icons/badges/icons8-attempt-made-black-50.png', + badge_tier=1 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-attempt-made-bronze-50.png', + badge_tier=2 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-attempt-made-silver-50.png', + badge_tier=3 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-attempt-made-gold-50.png', + badge_tier=4 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-calendar-2-50.png', + badge_tier=1 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-calendar-7-50.png', + badge_tier=2 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-calendar-14-50.png', + badge_tier=3 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-calendar-21-50.png', + badge_tier=4 ) Badge.objects.create( 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' + icon_name='img/icons/badges/icons8-calendar-28-50.png', + badge_tier=5 ) 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/models.py b/codewof/programming/models.py index 71629629c..c4917d187 100644 --- a/codewof/programming/models.py +++ b/codewof/programming/models.py @@ -58,6 +58,7 @@ class Badge(models.Model): 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) def __str__(self): return self.display_name diff --git a/codewof/tests/codewof_test_data_generator.py b/codewof/tests/codewof_test_data_generator.py index e1519ae10..6ef712915 100644 --- a/codewof/tests/codewof_test_data_generator.py +++ b/codewof/tests/codewof_test_data_generator.py @@ -4,9 +4,12 @@ """ from django.contrib.auth import get_user_model +from django.core.management import call_command from datetime import datetime from programming.models import Profile, Badge, Question, Attempt +from codewof.tests.users.factories import UserFactory +from codewof.tests.conftest import user from users.models import UserType @@ -17,10 +20,11 @@ def generate_questions(): Question.objects.create(title='Test', question_text='Hello') -def generate_users(): - usertype = UserType.objects.create(slug='student') - User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion', user_type=usertype) - User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion', user_type=usertype) +def generate_users(user): + # call_command("load_user_types") + user.create_batch(size=2) + # User.objects.create_user(username='john', email='john@uclive.ac.nz', password='onion', user_type=usertype) + # User.objects.create_user(username='sally', email='sally@uclive.ac.nz', password='onion', user_type=usertype) def generate_badges(): diff --git a/codewof/tests/programming/test_models.py b/codewof/tests/programming/test_models.py index 58405afe5..10457108a 100644 --- a/codewof/tests/programming/test_models.py +++ b/codewof/tests/programming/test_models.py @@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model from codewof.tests.codewof_test_data_generator import * +from codewof.tests.conftest import user User = get_user_model() @@ -37,7 +38,7 @@ class ProfileModelTests(TestCase): @classmethod def setUpTestData(cls): # never modify this object in tests - read only - generate_users() + generate_users(user) def test_profile_starts_with_no_points(self): user = User.objects.get(id=1) From 1d172bb0a64fbbcc8e5af0b621efdf7173e2eb41 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Thu, 19 Sep 2019 17:34:30 +1200 Subject: [PATCH 013/205] Added badge point calculations --- codewof/codewof/codewof_utils.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index 70474c696..8a863d669 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -95,7 +95,8 @@ def check_badge_conditions(user): """check badges for account creation, consecutive days with questions answered, attempts made, points earned, and questions solved""" earned_badges = user.profile.earned_badges.all() - new_badges = [] + new_badge_names = [] + new_badge_objects = [] # account creation badge try: creation_badge = Badge.objects.get(id_name="create-account") @@ -104,7 +105,8 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=creation_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(creation_badge.display_name) + new_badge_names.append(creation_badge.display_name) + new_badge_objects.append(creation_badge) except Badge.DoesNotExist: logger.warning("No such badge: create-account") pass @@ -120,7 +122,8 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=question_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(question_badge.display_name) + new_badge_names.append(question_badge.display_name) + new_badge_objects.append(question_badge) except Badge.DoesNotExist: logger.warning("No such badges: questions-solved") pass @@ -136,7 +139,8 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=attempt_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(attempt_badge.display_name) + new_badge_names.append(attempt_badge.display_name) + new_badge_objects.append(attempt_badge) except Badge.DoesNotExist: logger.warning("No such badges: attempts-made") pass @@ -153,10 +157,19 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=consec_badge) new_achievement.full_clean() new_achievement.save() - new_badges.append(consec_badge.display_name) - # user.profile = backdate_points(user.profile) - # backdate_badges(user.profile) - return new_badges + new_badge_names.append(consec_badge.display_name) + new_badge_objects.append(consec_badge) + + calculate_badge_points(user, new_badge_objects) + return new_badge_names + + +def calculate_badge_points(user, badges): + for badge in badges: + points = badge.badge_tier * 10 + user.profile.points += points + user.full_clean() + user.save() def backdate_points_and_badges(): From 7e83a04cf2301d718ceb3e0fb91f431e82c13053 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Thu, 19 Sep 2019 20:42:23 +1200 Subject: [PATCH 014/205] Toasts now showing. Custom text based on badge earned yet to be implemented due to not recognizing toast component IDs. --- codewof/static/js/question_types/base.js | 12 ++++++++---- codewof/templates/programming/question.html | 15 +++++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/codewof/static/js/question_types/base.js b/codewof/static/js/question_types/base.js index 17d5db670..64e4ebd8a 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({ @@ -32,10 +35,11 @@ function update_gamification(data) { console.log("pointsed") badges = data.badges; console.log(badges); - $("#toast-header").innerText = "New badges!"; - $("#toast-body").innerText = badges; - $("#badge-toast").toast({ - delay: 3000 + + $("#badge_toast_header").text = "New badges!"; + $("#badge_toast_body").text = badges; + $(document).ready(function(){ + $(".toast").toast('show'); }); console.log("toasted"); } diff --git a/codewof/templates/programming/question.html b/codewof/templates/programming/question.html index 159892b2f..04ab9ee63 100644 --- a/codewof/templates/programming/question.html +++ b/codewof/templates/programming/question.html @@ -55,12 +55,15 @@

{{ question.title }}

-
-
- Badge! -
-
- badge! +
+
{% endblock %} From 35a6839bae15e7a7bd0971f9103e5d97c05c0c06 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Thu, 19 Sep 2019 23:10:09 +1200 Subject: [PATCH 015/205] Added feature files to detail gamification tests. --- codewof/tests/features/badges.feature | 6 ++++++ codewof/tests/features/points.feature | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 codewof/tests/features/badges.feature create mode 100644 codewof/tests/features/points.feature diff --git a/codewof/tests/features/badges.feature b/codewof/tests/features/badges.feature new file mode 100644 index 000000000..ec9568404 --- /dev/null +++ b/codewof/tests/features/badges.feature @@ -0,0 +1,6 @@ +Feature: Badges + +Scenario: User profile is created + Given a user with ID 1 exists + When their account is created + Then their profile has 0 badges diff --git a/codewof/tests/features/points.feature b/codewof/tests/features/points.feature new file mode 100644 index 000000000..90305e91f --- /dev/null +++ b/codewof/tests/features/points.feature @@ -0,0 +1,8 @@ +Feature: Points + +Scenario: User profile is created + Given a user with ID 1 exists + When their account is created + Then their profile's points are equal to 0 + +Scenario: User answers From 08d7e8c431cdf60eb768f874cd4dbf00e4a3b47d Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Thu, 19 Sep 2019 23:24:47 +1200 Subject: [PATCH 016/205] Changed sampledata creation badge to be tier 0 (no points). Added more points feature tests for attempting and solving questions, and earning a badge. --- .../general/management/commands/sampledata.py | 2 +- codewof/tests/features/points.feature | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/codewof/general/management/commands/sampledata.py b/codewof/general/management/commands/sampledata.py index 807e8a2c2..22115d32a 100644 --- a/codewof/general/management/commands/sampledata.py +++ b/codewof/general/management/commands/sampledata.py @@ -101,7 +101,7 @@ def handle(self, *args, **options): display_name='Created an account!', description='Created your very own account', icon_name='img/icons/badges/icons8-badge-create-account-48.png', - badge_tier=1 + badge_tier=0 ) Badge.objects.create( diff --git a/codewof/tests/features/points.feature b/codewof/tests/features/points.feature index 90305e91f..9e1f211a6 100644 --- a/codewof/tests/features/points.feature +++ b/codewof/tests/features/points.feature @@ -1,8 +1,46 @@ Feature: Points +#Test initial points are zero Scenario: User profile is created - Given a user with ID 1 exists + Given a user with ID 1 is logged in When their account is created Then their profile's points are equal to 0 -Scenario: User answers +#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 11 + +#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 + 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 From 471e43a4726ddc3c4f3f66f500c39953be0b11c1 Mon Sep 17 00:00:00 2001 From: Maree Palmer Date: Fri, 20 Sep 2019 23:08:19 +1200 Subject: [PATCH 017/205] Removed activity graph from dashboard. Fixed toasts to show custom badge messages when achieved. --- codewof/codewof/codewof_utils.py | 23 ++++---- codewof/static/js/question_types/base.js | 17 +++--- codewof/static/scss/website.scss | 24 ++++++++ codewof/templates/programming/question.html | 6 +- codewof/templates/users/activity_graph.html | 65 +++++++++++++++++++++ codewof/templates/users/dashboard.html | 4 +- codewof/users/views.py | 5 +- 7 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 codewof/templates/users/activity_graph.html diff --git a/codewof/codewof/codewof_utils.py b/codewof/codewof/codewof_utils.py index 8a863d669..a0c524558 100644 --- a/codewof/codewof/codewof_utils.py +++ b/codewof/codewof/codewof_utils.py @@ -1,6 +1,7 @@ import datetime import json import logging +from dateutil.relativedelta import relativedelta from programming.models import ( Profile, @@ -42,7 +43,7 @@ def add_points(question, profile, attempt): points_to_add += 10 if len(num_attempts) == 1: # correct first try - points_to_add += 1 + points_to_add += 2 profile.points += points_to_add profile.full_clean() @@ -85,17 +86,17 @@ def get_days_consecutively_answered(user): def get_days_with_solutions(user): """Gets a list of dates with questions successfully answered.""" - today = datetime.datetime.now().replace(tzinfo=None).date() - attempts = Attempt.objects.filter(profile=user.profile, datetime__year=today.year, passed_tests=True).datetimes( - 'datetime', 'day', 'DESC') - return attempts + today = datetime.datetime.now().replace(tzinfo=None) + relativedelta(days=1) + four_weeks_ago = today - relativedelta(months=1) + attempts = Attempt.objects.filter(profile=user.profile, datetime__gte=four_weeks_ago.date(), passed_tests=True) + return len(attempts) def check_badge_conditions(user): """check badges for account creation, consecutive days with questions answered, attempts made, points earned, and questions solved""" earned_badges = user.profile.earned_badges.all() - new_badge_names = [] + new_badge_names = "" new_badge_objects = [] # account creation badge try: @@ -105,7 +106,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=creation_badge) new_achievement.full_clean() new_achievement.save() - new_badge_names.append(creation_badge.display_name) + 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") @@ -122,7 +123,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=question_badge) new_achievement.full_clean() new_achievement.save() - new_badge_names.append(question_badge.display_name) + 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") @@ -139,7 +140,7 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=attempt_badge) new_achievement.full_clean() new_achievement.save() - new_badge_names.append(attempt_badge.display_name) + 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") @@ -157,9 +158,9 @@ def check_badge_conditions(user): new_achievement = Earned(profile=user.profile, badge=consec_badge) new_achievement.full_clean() new_achievement.save() - new_badge_names.append(consec_badge.display_name) + 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 diff --git a/codewof/static/js/question_types/base.js b/codewof/static/js/question_types/base.js index 64e4ebd8a..bfa25e12c 100644 --- a/codewof/static/js/question_types/base.js +++ b/codewof/static/js/question_types/base.js @@ -32,16 +32,15 @@ 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. - console.log("pointsed") - badges = data.badges; - console.log(badges); - $("#badge_toast_header").text = "New badges!"; - $("#badge_toast_body").text = badges; - $(document).ready(function(){ - $(".toast").toast('show'); - }); - console.log("toasted"); + 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'); + }); + } } function display_submission_feedback(test_cases) { diff --git a/codewof/static/scss/website.scss b/codewof/static/scss/website.scss index 343b53f16..a19141ccb 100644 --- a/codewof/static/scss/website.scss +++ b/codewof/static/scss/website.scss @@ -248,3 +248,27 @@ $red: #b94a48; .orange-underline { border-bottom: 0.5rem $brand-secondary-colour solid; } + +.toast { + border: 1px solid $brand-colour; + background-colour: brand-colour-light; + text-align: center; +} + +.toast-header { + border: 1px solid $brand-colour; + background-colour: 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-colour: brand-colour-light; +} diff --git a/codewof/templates/programming/question.html b/codewof/templates/programming/question.html index 04ab9ee63..011a26480 100644 --- a/codewof/templates/programming/question.html +++ b/codewof/templates/programming/question.html @@ -56,10 +56,12 @@

{{ question.title }}

Return to dashboard
-