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

Challenges and leaderboards #3127

Merged
merged 40 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0ac48c7
team_overview page
krrish-sehgal Dec 17, 2024
880f674
team_overview page
krrish-sehgal Dec 17, 2024
205e250
team_overview page
krrish-sehgal Dec 17, 2024
9483ab0
merge
krrish-sehgal Dec 17, 2024
3e9ff05
merge
krrish-sehgal Dec 17, 2024
083079e
req changes made
krrish-sehgal Dec 17, 2024
44e7b58
Merge branch 'main' into team-overview
krrish-sehgal Dec 17, 2024
f87a519
changes
krrish-sehgal Dec 17, 2024
a783691
merge changes
krrish-sehgal Dec 18, 2024
8dc4c4c
merge changes
krrish-sehgal Dec 18, 2024
7e9c7bf
merge changes
krrish-sehgal Dec 18, 2024
1aef280
Merge branch 'main' of https://github.com/krrish-sehgal/Owasp-BLT int…
krrish-sehgal Dec 18, 2024
0a6ebb9
initial
krrish-sehgal Dec 18, 2024
2a920f6
fixes
krrish-sehgal Dec 18, 2024
d337bab
fixes
krrish-sehgal Dec 18, 2024
4b787ba
fixes
krrish-sehgal Dec 18, 2024
7467a01
fixes
krrish-sehgal Dec 18, 2024
d4d4c82
fixes
krrish-sehgal Dec 18, 2024
40a266b
final fixes
krrish-sehgal Dec 19, 2024
35b51e3
final fixes
krrish-sehgal Dec 19, 2024
4323018
final fixes
krrish-sehgal Dec 19, 2024
6d58c20
final fixes
krrish-sehgal Dec 19, 2024
80c5e17
final fixes
krrish-sehgal Dec 19, 2024
820db3f
Merge branch 'main' into challenges-leaderboard
krrish-sehgal Dec 19, 2024
bdde7ec
changes
krrish-sehgal Dec 29, 2024
f620993
Merge branch 'challenges-leaderboard' of https://github.com/krrish-se…
krrish-sehgal Dec 29, 2024
920c3a1
merge fiel
krrish-sehgal Dec 29, 2024
e8b2064
pre
krrish-sehgal Dec 29, 2024
380a15f
comments on blog added
krrish-sehgal Jan 4, 2025
f6dcd31
Merge branch 'OWASP-BLT:main' into main
krrish-sehgal Jan 8, 2025
81271b2
merged
krrish-sehgal Jan 8, 2025
3c6cb01
Merge branch 'main' into challenges-leaderboard
DonnieBLT Jan 19, 2025
196ab0c
pre on staged changes
krrish-sehgal Jan 24, 2025
6ec53b8
sync with main
krrish-sehgal Jan 24, 2025
dd921f8
reverting
krrish-sehgal Jan 24, 2025
dac9a28
changes synced
krrish-sehgal Jan 25, 2025
d36ab7d
minor changes
krrish-sehgal Jan 26, 2025
02cd16d
sync
krrish-sehgal Jan 26, 2025
f64465a
sync and pre
krrish-sehgal Jan 26, 2025
0a9a9db
Merge branch 'main' into challenges-leaderboard
krrish-sehgal Jan 26, 2025
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
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
Loading