Skip to content

Commit

Permalink
Challenges and leaderboards (OWASP-BLT#3127)
Browse files Browse the repository at this point in the history
  • Loading branch information
krrish-sehgal authored Jan 26, 2025
1 parent 3bd2a85 commit 399de4a
Show file tree
Hide file tree
Showing 21 changed files with 1,019 additions and 35 deletions.
6 changes: 6 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
)
from website.views.slack_handlers import slack_commands, slack_events
from website.views.teams import (
TeamChallenges,
TeamLeaderboard,
TeamOverview,
add_member,
create_team,
Expand All @@ -197,6 +199,7 @@
GlobalLeaderboardView,
InviteCreate,
SpecificMonthLeaderboardView,
UserChallengeListView,
UserDeleteView,
UserProfileDetailsView,
UserProfileDetailView,
Expand Down Expand Up @@ -852,6 +855,9 @@
name="similarity_scan",
),
path("projects/create/", create_project, name="create_project"),
path("teams/challenges/", TeamChallenges.as_view(), name="team_challenges"),
path("teams/leaderboard/", TeamLeaderboard.as_view(), name="team_leaderboard"),
path("challenges/", UserChallengeListView.as_view(), name="user_challenges"),
path("project/<slug:slug>/", ProjectsDetailView.as_view(), name="projects_detail"),
path("slack/events", slack_events, name="slack_events"),
path("owasp/", TemplateView.as_view(template_name="owasp.html"), name="owasp"),
Expand Down
3 changes: 2 additions & 1 deletion website/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ class WebsiteConfig(AppConfig):
name = "website"

def ready(self):
import website.signals # noqa
import website.challenge_signals # noqa
import website.feed_signals # noqa
199 changes: 199 additions & 0 deletions website/challenge_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone

from .models import Challenge, IpReport, Issue, Points, TimeLog, UserProfile


def update_challenge_progress(user, challenge_title, model_class, reason, threshold=None, team_threshold=None):
if not user.is_authenticated:
return
try:
challenge = Challenge.objects.get(title=challenge_title)

if challenge.challenge_type == "team":
# Get the user's team
user_profile = user.userprofile
if user_profile.team is None:
return

team = user_profile.team
if team not in challenge.team_participants.all():
challenge.team_participants.add(team)

total_actions = 0
for member in team.user_profiles.all():
total_actions += model_class.objects.filter(user=member.user).count()

# Calculate progress based on actions performed by the team
team_progress = min((total_actions / team_threshold) * 100, 100)

challenge.progress = int(team_progress)
challenge.save()

if team_progress == 100 and not challenge.completed:
challenge.completed = True # Explicitly mark the challenge as completed
challenge.completed_at = timezone.now() # Track completion time (optional)
challenge.save() # Save changes to the challenge

team.team_points += challenge.points
team.save()
else:
if user not in challenge.participants.all():
challenge.participants.add(user)

user_count = model_class.objects.filter(user=user).count()
progress = min((user_count / threshold) * 100, 100) # Ensure it doesn't exceed 100%

challenge.progress = int(progress)
challenge.save()
print(challenge.completed)
if challenge.progress == 100 and not challenge.completed:
challenge.completed = True # Explicitly mark the challenge as completed
challenge.completed_at = timezone.now()
challenge.save()

# Award points to the user
Points.objects.create(user=user, score=challenge.points, reason=reason)

except Challenge.DoesNotExist:
pass


@receiver(post_save)
def handle_post_save(sender, instance, created, **kwargs):
"""Generic handler for post_save signal."""
if sender == IpReport and created: # Track first IP report
if instance.user and instance.user.is_authenticated:
update_challenge_progress(
user=instance.user,
challenge_title="Report 5 IPs",
model_class=IpReport,
reason="Completed 'Report 5 IPs' challenge",
threshold=5,
)
if instance.user.is_authenticated and instance.user.userprofile.team:
update_challenge_progress(
user=instance.user,
challenge_title="Report 10 IPs",
model_class=IpReport,
reason="Completed 'Report 10 IPs challenge",
team_threshold=10, # For team challenge
)

elif sender == Issue and created: # Track first bug report
if instance.user and instance.user.is_authenticated:
update_challenge_progress(
user=instance.user,
challenge_title="Report 5 Issues",
model_class=Issue,
reason="Completed 'Report 5 Issues challenge",
threshold=5,
)
if instance.user.is_authenticated and instance.user.userprofile.team:
update_challenge_progress(
user=instance.user,
challenge_title="Report 10 Issues",
model_class=Issue,
reason="Completed 'Report 10 Issues challenge",
team_threshold=10, # For team challenge
)


@receiver(post_save, sender=TimeLog)
def update_user_streak(sender, instance, created, **kwargs):
if created and instance.user and instance.user.is_authenticated:
check_in_date = instance.start_time.date() # Extract the date from TimeLog
user = instance.user

try:
user_profile = user.userprofile
user_profile.update_streak_and_award_points(check_in_date)

handle_sign_in_challenges(user, user_profile)

if user_profile.team:
handle_team_sign_in_challenges(user_profile.team)

except UserProfile.DoesNotExist:
pass


def handle_sign_in_challenges(user, user_profile):
"""
Update progress for single challenges based on the user's streak.
"""
try:
print("Handling user sign-in challenge...")
challenge_title = "Sign in for 5 Days"
challenge = Challenge.objects.get(title=challenge_title, challenge_type="single")

if user not in challenge.participants.all():
challenge.participants.add(user)

streak_count = user_profile.current_streak
print(streak_count)

if streak_count >= 5:
progress = 100
else:
progress = streak_count * 100 / 5 # Calculate progress if streak is less than 5
print(progress)
# Update the challenge progress
challenge.progress = int(progress)
challenge.save()

# Award points if the challenge is completed (when streak is 5)
if progress == 100 and not challenge.completed:
challenge.completed = True
challenge.completed_at = timezone.now()
challenge.save()

Points.objects.create(
user=user,
score=challenge.points,
reason=f"Completed '{challenge_title}' challenge",
)

except Challenge.DoesNotExist:
# Handle case when the challenge does not exist
pass


def handle_team_sign_in_challenges(team):
"""
Update progress for team challenges where all members must sign in for 5 days consecutively.
"""
try:
challenge_title = "All Members Sign in for 5 Days" # Title of the team challenge
challenge = Challenge.objects.get(title=challenge_title, challenge_type="team")
print("Handling team sign-in challenge...")

# Ensure the team is registered as a participant
if team not in challenge.team_participants.all():
challenge.team_participants.add(team)

# Get streaks for all team members
streaks = [member.current_streak for member in team.user_profiles.all()]

if streaks: # If the team has members
min_streak = min(streaks)
progress = min((min_streak / 5) * 100, 100)
else:
min_streak = 0
progress = 0

challenge.progress = int(progress)
challenge.save()

if progress == 100 and not challenge.completed:
challenge.completed = True
challenge.completed_at = timezone.now()
challenge.save()

# Add points to the team
team.team_points += challenge.points
team.save()
except Challenge.DoesNotExist:
print(f"Challenge '{challenge_title}' does not exist.")
pass
7 changes: 2 additions & 5 deletions website/signals.py → website/feed_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def handle_post_save(sender, instance, created, **kwargs):
@receiver(pre_delete)
def handle_pre_delete(sender, instance, **kwargs):
"""Generic handler for pre_delete signal."""
if sender in [Issue, Hunt, IpReport, Post]: # Add any model you want to track
if sender in [Issue, Hunt, IpReport, Post]:
create_activity(instance, "deleted")


Expand All @@ -106,15 +106,12 @@ def update_user_streak(sender, instance, created, **kwargs):
"""
Automatically update user's streak when a TimeLog is created
"""
if created:
# Use the date of the start_time for streak tracking
if created and instance.user and instance.user.is_authenticated:
check_in_date = instance.start_time.date()
# Get the user's profile and update streak
try:
user_profile = instance.user.userprofile
user_profile.update_streak_and_award_points(check_in_date)
except UserProfile.DoesNotExist:
# Fallback: create profile if it doesn't exist
UserProfile.objects.create(
user=instance.user, current_streak=1, longest_streak=1, last_check_in=check_in_date
)
56 changes: 56 additions & 0 deletions website/migrations/0173_challenge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Generated by Django 5.1.3 on 2024-12-18 18:54

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("website", "0172_merge_20241218_0505"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Challenge",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=255)),
("description", models.TextField()),
(
"challenge_type",
models.CharField(
choices=[("single", "Single User"), ("team", "Team")],
default="single",
max_length=10,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("points", models.IntegerField(default=0)),
("progress", models.IntegerField(default=0)),
("completed", models.BooleanField(default=False)),
("completed_at", models.DateTimeField(blank=True, null=True)),
(
"participants",
models.ManyToManyField(
blank=True,
related_name="user_challenges",
to=settings.AUTH_USER_MODEL,
),
),
(
"team_participants",
models.ManyToManyField(blank=True, related_name="team_challenges", to="website.organization"),
),
],
),
]
48 changes: 48 additions & 0 deletions website/migrations/0174_add_single_user_challenges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 5.1.3 on 2024-12-18 09:39

from django.db import migrations

single_user_challenges = [
{
"title": "Report 5 IPs",
"description": "Report 5 different suspicious IPs to complete this challenge.",
"challenge_type": "single",
"points": 1,
},
{
"title": "Report 5 Issues",
"description": "Report 5 unique issues to complete this challenge.",
"challenge_type": "single",
"points": 1,
},
{
"title": "Sign in for 5 Days",
"description": "Sign in for 5 consecutive days to complete this challenge.",
"challenge_type": "single",
"points": 1,
},
]


def add_single_user_challenges(apps, schema_editor):
# Get the Challenge model
Challenge = apps.get_model("website", "Challenge")

# Loop through the challenges and create them
for challenge_data in single_user_challenges:
Challenge.objects.create(
title=challenge_data["title"],
description=challenge_data["description"],
challenge_type=challenge_data["challenge_type"],
points=challenge_data["points"],
)


class Migration(migrations.Migration):
dependencies = [
("website", "0173_challenge"),
]

operations = [
migrations.RunPython(add_single_user_challenges),
]
Loading

0 comments on commit 399de4a

Please sign in to comment.