Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v0.2 #11

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.urls import path

from accounts import views
from .views import SignUpView
from .views import SignUpView, delete_incomplete_turns

urlpatterns = [
# TODO: Django already provides some mechanism for account management in /accounts. Maybe its possible to reuse it.
Expand All @@ -11,4 +11,7 @@
path("profile/update_email/", views.update_email, name='update_email'),
path("profile/delete_account/", views.delete_account, name='delete_account'),
path("change-password/", auth_views.PasswordChangeView.as_view(), name='change_password'),

path("profile/leaderboard/", views.delete_account, name='leaderboard_personal'),
path('delete_incomplete_turns/', delete_incomplete_turns, name='delete_incomplete_turns'),
]
35 changes: 34 additions & 1 deletion accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from django.contrib.auth.forms import UserCreationForm
from django.http import JsonResponse
from django.urls import reverse_lazy
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import CreateView
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.contrib.auth import logout
from .forms import UpdateEmailForm, DeleteAccountForm
from quiz.models import QuizTurn
from quiz.views import get_user_progress


class SignUpView(CreateView):
Expand All @@ -16,7 +20,26 @@ class SignUpView(CreateView):

@login_required
def profile(request):
return render(request, 'user/profile.html')
user = request.user
turns = list(map(lambda t: (t.quiz.name, get_user_progress(t.user, t.quiz)),
QuizTurn.objects.filter(user=user, is_completed=True).order_by('quiz_id')))
temp_grouped_turns = {}

for quiz, info in turns:
if quiz not in temp_grouped_turns:
temp_grouped_turns[quiz] = []
temp_grouped_turns[quiz].append(info)

# Remove duplicates by converting lists of dictionaries to sets of frozensets and back to lists of dictionaries
for quiz in temp_grouped_turns:
unique_dicts = {frozenset(d.items()) for d in temp_grouped_turns[quiz]}
temp_grouped_turns[quiz] = [dict(d) for d in unique_dicts]

grouped_turn = [{'quiz': q, 'data': i} for q, i in temp_grouped_turns.items()]

pending_turns = QuizTurn.objects.filter(user=user, is_completed=False).count()
return render(request, 'user/profile.html',
{'data': grouped_turn, 'pending_turns': pending_turns})


@login_required
Expand Down Expand Up @@ -45,3 +68,13 @@ def delete_account(request):
else:
form = DeleteAccountForm()
return render(request, 'user/delete_account.html', {'form': form})


@login_required
@csrf_exempt
def delete_incomplete_turns(request):
if request.method == 'POST':
user = request.user
QuizTurn.objects.filter(user=user, is_completed=False).delete()
return JsonResponse({'status': 'ok'})
return JsonResponse({'status': 'error'}, status=400)
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# JQuiz Changelog

## 0.2

- Added profile ability to delete pending quiz turns.
- Added personal progress overview to profile.
- Reworked leaderboard to be more encouraging.

## 0.1

- Added prototypic user account management
Expand Down
2 changes: 1 addition & 1 deletion quiz/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class UserAnswerAdmin(admin.ModelAdmin):


class QuizAdmin(admin.ModelAdmin):
list_display = ["name"]
list_display = ["id", "name"]
fieldsets = [
(None, {"fields": ["name"]}),
]
Expand Down
10 changes: 10 additions & 0 deletions quiz/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ def __str__(self):
state = "completed" if self.is_completed else "pending"
return f"Turn {self.id} {state} {self.user.username} : {self.quiz}"

def is_correct(self):
"""
Returns true if the number of correct user answers matches the number of total questions for
the related quiz questions.
"""
correct_user_answers = UserAnswer.objects.filter(turn=self, selected_choice__is_correct=True).values(
'question').distinct().count()
total_questions = Question.objects.filter(related_quiz=self.quiz).count()
return correct_user_answers == total_questions


class UserAnswer(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
Expand Down
69 changes: 50 additions & 19 deletions quiz/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render
Expand All @@ -17,7 +18,7 @@ def get_queryset(self):
return Question.objects.order_by("-submit_date")[:5]


class DetailView(generic.DetailView):
class DetailView(LoginRequiredMixin, generic.DetailView):
"""
Shows all details of a single Question
"""
Expand All @@ -36,43 +37,73 @@ def get_context_data(self, **kwargs):

def leaderboard_overall_view(request):
quizzes = Quiz.objects.all()
return render(request, 'quiz/leaderboard.html',
return render(request, 'leaderboard/leaderboard.html',
{'leaderboard_data': list(map(lambda q: _get_leaderboard(q), quizzes))})


def leaderboard_quiz_view(request, quiz_id):
quiz = get_object_or_404(Quiz, id=quiz_id)
return render(request, 'quiz/leaderboard_quiz.html', {'leaderboard_data': _get_leaderboard(quiz)})
return render(request, 'leaderboard/leaderboard_quiz.html', {
'user_progress': get_user_progress(request.user, quiz),
'leaderboard_data': _get_leaderboard(quiz),
})


def _get_leaderboard(quiz):
turns = QuizTurn.objects.filter(quiz=quiz, is_completed=True)
user_scores = list(map(lambda turn: _get_user_score(turn), turns))
user_scores = sorted(user_scores, key=lambda t: t["total_correct"], reverse=True)
distinct_key = "username"
seen_keys = set()
distinct_data = []
key_avg_score = 'average_score'
user_scores = sorted(user_scores, key=_get_user_score_rank)

distinct_scores = {}
for d in user_scores:
k = d[distinct_key]
if k not in seen_keys:
distinct_data.append(d)
seen_keys.add(k)
username = d['username']
if username not in distinct_scores:
distinct_scores[username] = d

return {
'quiz': quiz,
'user_scores': distinct_data
'user_scores': list(distinct_scores.values()),
'average_quiz_score': round(
sum(d[key_avg_score] for d in user_scores) / len(user_scores) if user_scores else 0,
2),
}


def _get_user_score_rank(user_score):
"""
- Primary: best_score
- Secondary: average_score
- Tertiary: correct_turns
"""
rank = (-user_score['best_score'], -user_score['average_score'], -user_score['correct_turns'])
return rank


def _get_user_score(turn):
correct_user_answers = (UserAnswer.objects.filter(turn=turn, selected_choice__is_correct=True)
.values('question').distinct().count())
return get_user_progress(turn.user, turn.quiz) | {
"username": turn.user.username
}


def _get_score(turn):
correct_user_answers = UserAnswer.objects.filter(turn=turn, selected_choice__is_correct=True).values(
'question').distinct().count()
total_questions = Question.objects.filter(related_quiz=turn.quiz).count()
return {"total_correct": correct_user_answers,
"total_possible": total_questions,
"score_percentage": correct_user_answers * 100.0 / total_questions,
"username": turn.user.username
}
return correct_user_answers * 100.0 / total_questions


def get_user_progress(user, quiz):
turns = QuizTurn.objects.filter(user=user, quiz=quiz, is_completed=True)
scores = [_get_score(turn) for turn in turns]
best_score = max(scores, default=0)
average_score = sum(scores) / len(scores) if scores else 0
return {
'correct_turns': len([turn for turn in turns if turn.is_correct()]),
'completed_turns': len(turns),
'best_score': round(best_score, 2),
'average_score': round(average_score, 2),
}


# --------------------------------------- Quiz ----------------------------------
Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JQuiz v0.1
# JQuiz v0.2

JQuiz is a web platform where users can answer multiple-choice questions related to Java in a competitive manner.
JQuiz is a web platform where users can answer multiple-choice questions related to Java in an encouraging manner.

## Requirements

Expand Down
50 changes: 50 additions & 0 deletions templates/leaderboard/leaderboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{% extends "leaderboard/leaderboard_base.html" %}

{% block title %}Overall Leaderboard{% endblock %}

{% block content %}
<div id="leaderboard">
<h1>Leaderboard</h1>
{% for quiz_data in leaderboard_data %}
<h2>{{ quiz_data.quiz.name }}</h2>
<table>
<thead>
<tr>
<th>Rank</th>
<th>User</th>
<th>Correct</th>
<th>Best Score</th>
<th>Average Score</th>
</tr>
</thead>
<tbody>
{% for score in quiz_data.user_scores %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ score.username }}</td>
<td>{{ score.correct_turns }}</td>
<td>
<div class="progress-bar">
<div class="progress"
style="width: {{ score.best_score }}%;">{{ score.best_score }}%
</div>
</div>
</td>
<td>
<div class="progress-bar">
<div class="progress"
style="width: {{ score.average_score }}%;">{{ score.average_score }}%
</div>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5">No users have completed this quiz yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
</div>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,30 @@
font-weight: bold;
color: #13A256;
}

.progress-bar {
width: 100%;
background-color: #333;
border-radius: 5px;
overflow: hidden;
height: 20px;
}

.progress {
height: 100%;
background-color: #13A256;
text-align: center;
color: white;
line-height: 20px;
white-space: nowrap;
padding: 0 5px;
}

.user-progress-table {
width: 100%;
margin-bottom: 20px;
border-collapse: collapse;
}

</style>
{% endblock %}
83 changes: 83 additions & 0 deletions templates/leaderboard/leaderboard_quiz.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{% extends "leaderboard/leaderboard_base.html" %}

{% block title %}{{ leaderboard_data.quiz.name }} - Leaderboard{% endblock %}


{% block content %}
<div id="leaderboard">
<h1>{{ leaderboard_data.quiz.name }} - Leaderboard</h1>

<!-- Your Progress Section -->
<h2>Your Progress</h2>
<table>
<tr>
<th>Correct Quizzes</th>
<th>Completed Quizzes</th>
<th>Best Score</th>
<th>Average Score</th>
</tr>
<tr>
<td>{{ user_progress.correct_turns }}</td>
<td>{{ user_progress.completed_turns }}</td>
<td>
<div class="progress-bar">
<div class="progress"
style="width: {{ user_progress.best_score }}%;">{{ user_progress.best_score }}%
</div>
</div>
</td>
<td>
<div class="progress-bar">
<div class="progress"
style="width: {{ user_progress.average_score }}%;">{{ user_progress.average_score }}%
</div>
</div>
</td>
</tr>
</table>

<!-- Existing Leaderboard Section -->
<h2>Quiz Leaderboard</h2>
<p>Overall Average Score for Quiz: {{ leaderboard_data.average_quiz_score }}%</p>
<table>
<thead>
<tr>
<th>Rank</th>
<th>User</th>
<th>Correct Quizzes</th>
<th>Best Score</th>
<th>Average Score</th>
</tr>
</thead>
<tbody>
{% for score in leaderboard_data.user_scores %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ score.username }}</td>
<td>{{ score.correct_turns }}</td>
<td>
<div class="progress-bar">
<div class="progress"
style="width: {{ score.best_score }}%;">{{ score.best_score }}%
</div>
</div>
</td>
<td>
<div class="progress-bar">
<div class="progress"
style="width: {{ score.average_score }}%;">{{ score.average_score }}%
</div>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5">No users have completed this quiz yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}


Loading