Skip to content
Merged
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
24 changes: 24 additions & 0 deletions website/challenge_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ def update_challenge_progress(user, challenge_title, model_class, reason, thresh

team.team_points += challenge.points
team.save()

# Award BACON tokens to all team members
from website.feed_signals import giveBacon

for team_member in team.user_profiles.all():
if team_member.user:
giveBacon(team_member.user, amt=challenge.bacon_reward)
else:
if user not in challenge.participants.all():
challenge.participants.add(user)
Expand All @@ -56,6 +63,11 @@ def update_challenge_progress(user, challenge_title, model_class, reason, thresh
# Award points to the user
Points.objects.create(user=user, score=challenge.points, reason=reason)

# Award BACON tokens for completing the challenge
from website.feed_signals import giveBacon

giveBacon(user, amt=challenge.bacon_reward)

except Challenge.DoesNotExist:
pass

Expand Down Expand Up @@ -155,6 +167,11 @@ def handle_sign_in_challenges(user, user_profile):
reason=f"Completed '{challenge_title}' challenge",
)

# Award BACON tokens for completing the challenge
from website.feed_signals import giveBacon

giveBacon(user, amt=challenge.bacon_reward)

except Challenge.DoesNotExist:
# Handle case when the challenge does not exist
pass
Expand Down Expand Up @@ -194,6 +211,13 @@ def handle_team_sign_in_challenges(team):
# Add points to the team
team.team_points += challenge.points
team.save()

# Award BACON tokens to all team members
from website.feed_signals import giveBacon

for team_member in team.user_profiles.all():
if team_member.user:
giveBacon(team_member.user, amt=challenge.bacon_reward)
except Challenge.DoesNotExist:
print(f"Challenge '{challenge_title}' does not exist.")
pass
52 changes: 52 additions & 0 deletions website/migrations/0243_add_bacon_rewards_to_challenges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Generated by Django 5.1.8 on 2025-07-25 10:00

from django.db import migrations, models


def update_challenge_bacon_rewards(apps, schema_editor):
"""Add bacon rewards to existing challenges"""
Challenge = apps.get_model("website", "Challenge")

# Define bacon rewards for different challenges
challenge_rewards = {
"Report 5 IPs": 10,
"Report 5 Issues": 15,
"Sign in for 5 Days": 5,
"Report 10 IPs": 20,
"Report 10 Issues": 25,
"All Members Sign in for 5 Days": 10,
}

# Update existing challenges
for challenge_title, bacon_reward in challenge_rewards.items():
try:
challenge = Challenge.objects.get(title=challenge_title)
challenge.bacon_reward = bacon_reward
challenge.save()
print(f"Updated challenge '{challenge_title}' with {bacon_reward} bacon reward")
except Challenge.DoesNotExist:
print(f"Challenge '{challenge_title}' not found, skipping...")


def reverse_update_challenge_bacon_rewards(apps, schema_editor):
"""Reverse operation - reset bacon_reward to default value"""
Challenge = apps.get_model("website", "Challenge")
Challenge.objects.all().update(bacon_reward=5)


class Migration(migrations.Migration):
dependencies = [
("website", "0242_labs"),
]

operations = [
migrations.AddField(
model_name="challenge",
name="bacon_reward",
field=models.IntegerField(default=5, help_text="BACON tokens earned for completing the challenge"),
),
migrations.RunPython(
update_challenge_bacon_rewards,
reverse_update_challenge_bacon_rewards,
),
]
1 change: 1 addition & 0 deletions website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,7 @@ class Challenge(models.Model):
Organization, related_name="team_challenges", blank=True
) # For team challenges
points = models.IntegerField(default=0) # Points for completing the challenge
bacon_reward = models.IntegerField(default=5, help_text="BACON tokens earned for completing the challenge")
progress = models.IntegerField(default=0) # Progress in percentage
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(null=True, blank=True)
Expand Down
10 changes: 8 additions & 2 deletions website/templates/team_challenges.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
</div>
<div>
<h1 class="text-3xl font-bold text-gray-800">Team Challenges</h1>
<p class="text-gray-600 mt-2">Complete challenges with your team to earn points and climb the leaderboard</p>
<p class="text-gray-600 mt-2">
Complete challenges with your team to earn points, BACON tokens, and climb the leaderboard
</p>
</div>
</div>
</div>
Expand All @@ -29,8 +31,12 @@ <h1 class="text-3xl font-bold text-gray-800">Team Challenges</h1>
</div>
<div>
<h3 class="text-lg font-bold text-gray-800">{{ challenge.title }}</h3>
<div class="flex items-center gap-2 mt-1">
<div class="flex items-center gap-4 mt-1">
<span class="text-sm font-medium text-[#e74c3c]">{{ challenge.points }} points</span>
<div class="flex items-center gap-1">
<i class="fas fa-bacon text-[#e74c3c] text-sm"></i>
<span class="text-sm font-medium text-[#e74c3c]">{{ challenge.bacon_reward }} BACON</span>
</div>
{% if challenge.progress >= 100 %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<i class="fas fa-check-circle mr-1"></i>
Expand Down
150 changes: 99 additions & 51 deletions website/templates/user_challenges.html
Original file line number Diff line number Diff line change
@@ -1,58 +1,106 @@
{% extends "base.html" %}
{% block content %}
{% include "includes/sidenav.html" %}
<div class="text-center mx-auto my-5 p-5 bg-gray-100 rounded-lg max-w-4xl shadow-md">
<h2 class="text-xl font-bold">Single User Challenges</h2>
{% if challenges %}
<div>
{% for challenge in challenges %}
<div class="my-4 p-4 bg-white rounded-lg border border-gray-300 text-left cursor-pointer shadow-sm hover:shadow-md transition-shadow duration-300"
onclick="toggleDetails({{ challenge.id }})">
<div class="flex justify-between items-center">
<span class="text-lg font-bold flex-grow">{{ challenge.title }}</span>
<span class="text-sm text-gray-500 font-bold mr-1">{{ challenge.points }} pts</span>
<span id="dropdown-icon-{{ challenge.id }}"
class="fas fa-chevron-down text-base transition-transform duration-300"></span>
</div>
<div id="challenge-details-{{ challenge.id }}"
class="hidden mt-2 text-sm text-gray-600">
<p>{{ challenge.description }}</p>
</div>
<!-- Progress Bar always visible -->
<div class="relative w-full bg-gray-300 rounded mt-2 h-5">
<div class="h-full bg-red-600 rounded transition-all duration-500"
id="progress-bar-{{ challenge.id }}"
data-progress="{{ challenge.progress }}"></div>
<span class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 font-bold text-gray-800">{{ challenge.progress|floatformat:0 }}%</span>
</div>
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<!-- Header Section -->
<div class="bg-white rounded-2xl shadow-lg p-8 mb-8">
<div class="flex items-center gap-6">
<div class="w-16 h-16 bg-[#e74c3c] rounded-full flex items-center justify-center">
<i class="fas fa-user-check text-2xl text-white"></i>
</div>
{% endfor %}
<div>
<h1 class="text-3xl font-bold text-gray-800">Individual Challenges</h1>
<p class="text-gray-600 mt-2">Complete personal challenges to earn points, BACON tokens, and showcase your skills</p>
</div>
</div>
</div>
{% else %}
<p class="text-gray-700">No challenges available.</p>
{% endif %}
{% if challenges %}
<div class="space-y-4">
{% for challenge in challenges %}
<div class="bg-white rounded-xl shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden"
x-data="{ open: false }">
<!-- Challenge Header -->
<div class="p-6 cursor-pointer" @click="open = !open">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-[#e74c3c] bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas fa-medal text-[#e74c3c] text-xl"></i>
</div>
<div>
<h3 class="text-lg font-bold text-gray-800">{{ challenge.title }}</h3>
<div class="flex items-center gap-4 mt-1">
<span class="text-sm font-medium text-[#e74c3c]">{{ challenge.points }} points</span>
<div class="flex items-center gap-1">
<i class="fas fa-bacon text-[#e74c3c] text-sm"></i>
<span class="text-sm font-medium text-[#e74c3c]">{{ challenge.bacon_reward }} BACON</span>
</div>
{% if challenge.progress >= 100 %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<i class="fas fa-check-circle mr-1"></i>
Completed
</span>
{% endif %}
</div>
</div>
</div>
<div class="flex items-center gap-4">
<!-- Progress Circle -->
<div class="relative w-12 h-12">
<svg class="w-12 h-12 transform -rotate-90">
<circle class="text-gray-200" stroke-width="3" stroke="currentColor" fill="transparent" r="20" cx="24" cy="24" />
<circle class="text-[#e74c3c]" stroke-width="3" stroke-linecap="round" stroke="currentColor" fill="transparent" r="20" cx="24" cy="24" style="stroke-dasharray: {{ challenge.stroke_dasharray }}; stroke-dashoffset: {{ challenge.stroke_dashoffset }}" />
</svg>
<span class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-sm font-medium">
{{ challenge.progress|floatformat:0 }}%
</span>
</div>
<i class="fas fa-chevron-down text-gray-400 transform transition-transform duration-300"
:class="{ 'rotate-180': open }"></i>
</div>
</div>
</div>
<!-- Challenge Details -->
<div class="overflow-hidden transition-all duration-300 max-h-0"
x-ref="content"
x-bind:style="open ? 'max-height: ' + $refs.content.scrollHeight + 'px' : ''">
<div class="p-6 pt-0 border-t border-gray-100">
<p class="text-gray-600">{{ challenge.description }}</p>
<div class="mt-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-user text-gray-400"></i>
</div>
<span class="text-sm font-medium text-gray-700">Personal Challenge</span>
</div>
{% if challenge.progress < 100 %}
<button class="px-4 py-2 bg-[#e74c3c] text-white rounded-lg hover:bg-[#c0392b] transition-colors duration-300">
Start Challenge
</button>
{% else %}
<div class="px-4 py-2 bg-green-100 text-green-800 rounded-lg">
<i class="fas fa-trophy mr-2"></i>
Challenge Completed!
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<!-- No Challenges State -->
<div class="bg-white rounded-xl shadow-md p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<i class="fas fa-clipboard-list text-2xl text-gray-400"></i>
</div>
<h3 class="text-xl font-semibold text-gray-800 mb-2">No Challenges Available</h3>
<p class="text-gray-600">There are no individual challenges available at the moment. Check back later!</p>
</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set progress bar widths based on data-progress attribute
const progressBars = document.querySelectorAll('[data-progress]');
progressBars.forEach(bar => {
const progress = bar.getAttribute('data-progress');
bar.style.width = progress + '%';
});
});

function toggleDetails(challengeId) {
const detailsElement = document.getElementById(`challenge-details-${challengeId}`);
const iconElement = document.getElementById(`dropdown-icon-${challengeId}`);

if (detailsElement.classList.contains("hidden")) {
detailsElement.classList.remove("hidden");
iconElement.classList.add("transform", "rotate-180");
} else {
detailsElement.classList.add("hidden");
iconElement.classList.remove("transform", "rotate-180");
}
}
</script>
<!-- Alpine.js for animations -->
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
{% endblock content %}
5 changes: 5 additions & 0 deletions website/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,11 @@ def get(self, request):
# If the user is not participating, set progress to 0
challenge.progress = 0

# Calculate the progress circle offset (same as team challenges)
circumference = 125.6
challenge.stroke_dasharray = circumference
challenge.stroke_dashoffset = circumference - (circumference * challenge.progress / 100)

return render(
request,
"user_challenges.html",
Expand Down