Skip to content

Commit 706d804

Browse files
committed
Add comprehensive quiz system with leaderboard integration
Add Quiz, QuizQuestion, and UserScore models for tracking quiz sessions and scores - Implement quiz functionality with 15 questions per category (5 easy, 5 medium, 5 hard) - Add quiz views: start_quiz, take_quiz, submit_quiz_answer, quiz_results - Create interactive quiz templates with progress bar and timer - Implement real-time scoring system that saves UserScore on quiz completion - Add leaderboard view displaying top 10 scorers per category with rankings - Integrate user scores display in navigation bar via context processor - Fix template syntax error by replacing invalid 'mul' filter with 'widthratio' - Remove Quiz model unique constraint to allow multiple quizzes per user/category - Add comprehensive error handling and debug logging for quiz completion - Ensure quiz scores are immediately visible in leaderboard and navigation Features: - Category-based quizzes (crypto, network, web, general) - Difficulty-based question selection (easy/medium/hard) - Real-time progress tracking and scoring - Best score tracking per category - Leaderboard with rankings 1-10 - User total score display in navigation - Quiz completion with automatic score saving - Responsive quiz interface with timer and progress bar
1 parent d9f42df commit 706d804

15 files changed

Lines changed: 2163 additions & 17 deletions

core/settings.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,7 @@
173173
"django.contrib.messages.context_processors.messages",
174174
'home.context_processors.dynamic_page_title',
175175
'home.context_processors.recaptcha_site_key',
176-
177-
176+
'home.context_processors.user_scores',
178177
],
179178
},
180179
},

home/context_processors.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# home/context_processors.py
22
from django.utils.text import capfirst
33
from django.conf import settings
4+
from .models import UserScore
45

56
def dynamic_page_title(request):
67
# Extract the path and capitalize it
@@ -31,4 +32,15 @@ def dynamic_page_title(request):
3132
def recaptcha_site_key(request):
3233
return {
3334
'RECAPTCHA_SITE_KEY': settings.RECAPTCHA_SITE_KEY
34-
}
35+
}
36+
37+
def user_scores(request):
38+
"""Add user scores to context for navigation display"""
39+
if request.user.is_authenticated:
40+
user_scores = UserScore.objects.filter(user=request.user).order_by('-score')
41+
total_score = sum(score.score for score in user_scores)
42+
return {
43+
'user_scores': user_scores,
44+
'user_total_score': total_score
45+
}
46+
return {}

home/management/commands/create_sample_challenges.py

Lines changed: 715 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 4.2.14 on 2025-09-16 07:56
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('home', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='Quiz',
17+
fields=[
18+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('category', models.CharField(choices=[('network', 'Network Security'), ('web', 'Web Application Security'), ('crypto', 'Cryptography'), ('general', 'General Knowledge'), ('python', 'Python'), ('javascript', 'JavaScript'), ('html_css', 'HTML & CSS'), ('web_security', 'Web Security'), ('reverse_engineering', 'Reverse Engineering'), ('forensics', 'Forensics'), ('binary_exploitation', 'Binary Exploitation'), ('linux', 'Linux'), ('algorithms', 'Algorithms'), ('data_structures', 'Data Structures'), ('databases', 'Databases'), ('regex', 'Regex'), ('secure_coding', 'Secure Coding'), ('logic_reasoning', 'Logic & Reasoning'), ('misc', 'Miscellaneous')], max_length=20)),
20+
('started_at', models.DateTimeField(auto_now_add=True)),
21+
('completed_at', models.DateTimeField(blank=True, null=True)),
22+
('is_completed', models.BooleanField(default=False)),
23+
('total_score', models.IntegerField(default=0)),
24+
('questions_answered', models.IntegerField(default=0)),
25+
('current_question_index', models.IntegerField(default=0)),
26+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
27+
],
28+
options={
29+
'unique_together': {('user', 'category')},
30+
},
31+
),
32+
migrations.CreateModel(
33+
name='QuizQuestion',
34+
fields=[
35+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36+
('question_order', models.IntegerField()),
37+
('user_answer', models.CharField(blank=True, max_length=200, null=True)),
38+
('is_correct', models.BooleanField(default=False)),
39+
('answered_at', models.DateTimeField(blank=True, null=True)),
40+
('challenge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.cyberchallenge')),
41+
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='home.quiz')),
42+
],
43+
options={
44+
'ordering': ['question_order'],
45+
'unique_together': {('quiz', 'question_order')},
46+
},
47+
),
48+
]

home/migrations/0003_userscore.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 4.2.14 on 2025-09-16 08:10
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('home', '0002_quiz_quizquestion'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='UserScore',
17+
fields=[
18+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('category', models.CharField(choices=[('network', 'Network Security'), ('web', 'Web Application Security'), ('crypto', 'Cryptography'), ('general', 'General Knowledge'), ('python', 'Python'), ('javascript', 'JavaScript'), ('html_css', 'HTML & CSS'), ('web_security', 'Web Security'), ('reverse_engineering', 'Reverse Engineering'), ('forensics', 'Forensics'), ('binary_exploitation', 'Binary Exploitation'), ('linux', 'Linux'), ('algorithms', 'Algorithms'), ('data_structures', 'Data Structures'), ('databases', 'Databases'), ('regex', 'Regex'), ('secure_coding', 'Secure Coding'), ('logic_reasoning', 'Logic & Reasoning'), ('misc', 'Miscellaneous')], max_length=20)),
20+
('score', models.IntegerField(default=0)),
21+
('quiz_completed_at', models.DateTimeField(auto_now_add=True)),
22+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
23+
],
24+
options={
25+
'ordering': ['-score', '-quiz_completed_at'],
26+
'unique_together': {('user', 'category')},
27+
},
28+
),
29+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.14 on 2025-09-16 09:38
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('home', '0003_userscore'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='quiz',
15+
options={'ordering': ['-started_at']},
16+
),
17+
migrations.AlterUniqueTogether(
18+
name='quiz',
19+
unique_together=set(),
20+
),
21+
]

home/models.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,53 @@ def __str__(self):
422422
return self.title
423423

424424

425+
class UserScore(models.Model):
426+
"""Model to track user scores for each category"""
427+
user = models.ForeignKey(User, on_delete=models.CASCADE)
428+
category = models.CharField(max_length=20, choices=CyberChallenge.CATEGORY_CHOICES)
429+
score = models.IntegerField(default=0)
430+
quiz_completed_at = models.DateTimeField(auto_now_add=True)
431+
432+
class Meta:
433+
unique_together = ['user', 'category']
434+
ordering = ['-score', '-quiz_completed_at']
435+
436+
def __str__(self):
437+
return f"{self.user.first_name} {self.user.last_name} - {self.category}: {self.score}"
438+
439+
class Quiz(models.Model):
440+
"""Model to track quiz sessions for each category"""
441+
user = models.ForeignKey(User, on_delete=models.CASCADE)
442+
category = models.CharField(max_length=20, choices=CyberChallenge.CATEGORY_CHOICES)
443+
started_at = models.DateTimeField(auto_now_add=True)
444+
completed_at = models.DateTimeField(null=True, blank=True)
445+
is_completed = models.BooleanField(default=False)
446+
total_score = models.IntegerField(default=0)
447+
questions_answered = models.IntegerField(default=0)
448+
current_question_index = models.IntegerField(default=0)
449+
450+
class Meta:
451+
ordering = ['-started_at']
452+
453+
def __str__(self):
454+
return f"{self.user.username} - {self.category} Quiz"
455+
456+
class QuizQuestion(models.Model):
457+
"""Model to track individual quiz questions and answers"""
458+
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='questions')
459+
challenge = models.ForeignKey(CyberChallenge, on_delete=models.CASCADE)
460+
question_order = models.IntegerField()
461+
user_answer = models.CharField(max_length=200, blank=True, null=True)
462+
is_correct = models.BooleanField(default=False)
463+
answered_at = models.DateTimeField(null=True, blank=True)
464+
465+
class Meta:
466+
unique_together = ['quiz', 'question_order']
467+
ordering = ['question_order']
468+
469+
def __str__(self):
470+
return f"{self.quiz} - Q{self.question_order}"
471+
425472
class UserChallenge(models.Model):
426473
user = models.ForeignKey(User, on_delete=models.CASCADE)
427474
challenge = models.ForeignKey(CyberChallenge, on_delete=models.CASCADE)

home/templates/includes/navigation.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,14 @@
359359
<span class="header-profile-name">{{ request.user.first_name }} {{ request.user.last_name }}</span>
360360
</a>
361361

362+
<!-- User Score Display -->
363+
{% if user_total_score > 0 %}
364+
<div class="user-score-display">
365+
<span class="score-label">Score:</span>
366+
<span class="score-value">{{ user_total_score }}</span>
367+
</div>
368+
{% endif %}
369+
362370

363371
{% else %}
364372
<div class="sign-up-button-container">
@@ -458,6 +466,35 @@
458466
<style>
459467
.header-profile-name { color: #1a1a1a !important; }
460468
body.dark-mode .header-profile-name { color: #fff !important; }
469+
470+
.user-score-display {
471+
display: inline-flex;
472+
align-items: center;
473+
background-color: #ffa500;
474+
color: #000;
475+
padding: 6px 12px;
476+
border-radius: 20px;
477+
margin-left: 10px;
478+
font-weight: bold;
479+
font-size: 14px;
480+
}
481+
482+
.score-label {
483+
margin-right: 5px;
484+
}
485+
486+
.score-value {
487+
background-color: #000;
488+
color: #ffa500;
489+
padding: 2px 8px;
490+
border-radius: 10px;
491+
font-weight: bold;
492+
}
493+
494+
body.dark-mode .user-score-display {
495+
background-color: #ffa500;
496+
color: #000;
497+
}
461498
</style>
462499

463500
<style>

home/templates/pages/challenges/category_challenges.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,23 @@ <h2 class="category-heading">{{ category|title }} Challenges</h2>
145145
<a href="{% url 'challenge_list' %}" class="back-button">← Back to Categories</a>
146146
</div>
147147

148+
<!-- Quiz Option -->
149+
<div class="row mb-4">
150+
<div class="col-12">
151+
<div class="card" style="background-color: #34344a; border: 2px solid #ffa500; border-radius: 16px;">
152+
<div class="card-body text-center">
153+
<h4 class="text-white mb-3">🎯 Take Category Quiz</h4>
154+
<p class="text-white-50 mb-3">
155+
Test your knowledge with 15 questions (5 Easy, 5 Medium, 5 Hard) from {{ category|title }} category.
156+
</p>
157+
<a href="{% url 'start_quiz' category %}" class="btn btn-warning btn-lg px-4">
158+
<i class="fas fa-play-circle me-2"></i>Start Quiz
159+
</a>
160+
</div>
161+
</div>
162+
</div>
163+
</div>
164+
148165
<div class="row mt-4">
149166
{% for challenge in challenges %}
150167
<div class="col-md-6 col-lg-4 mb-4 d-flex">

0 commit comments

Comments
 (0)