From 3bd2a858417e0ab263739d1aa29bb2b2740c506e Mon Sep 17 00:00:00 2001 From: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> Date: Sun, 26 Jan 2025 05:17:22 -0500 Subject: [PATCH 1/2] Create an /owasp page (#3276) * Create an /owasp page Related to #3275 --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/OWASP-BLT/BLT/issues/3275?shareId=XXXX-XXXX-XXXX-XXXX). * Apply pre-commit fixes * Update owasp.html --------- Co-authored-by: github-actions[bot] --- blt/urls.py | 1 + website/templates/owasp.html | 172 +++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 website/templates/owasp.html diff --git a/blt/urls.py b/blt/urls.py index 37cfdef7c..31cd29345 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -854,6 +854,7 @@ path("projects/create/", create_project, name="create_project"), path("project//", 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"), ] if settings.DEBUG: diff --git a/website/templates/owasp.html b/website/templates/owasp.html new file mode 100644 index 000000000..12664b03a --- /dev/null +++ b/website/templates/owasp.html @@ -0,0 +1,172 @@ +{% extends "base.html" %} +{% load static %} +{% load custom_tags %} +{% block title %} + Welcome to OWASP +{% endblock title %} +{% block description %} + A guide for new community members to get involved with OWASP. +{% endblock description %} +{% block keywords %} + OWASP, security, open source, community, software security, projects, chapters, membership +{% endblock keywords %} +{% block og_title %} + Welcome to OWASP +{% endblock og_title %} +{% block og_description %} + Learn how to get involved with OWASP, join local chapters, contribute to projects, and more. +{% endblock og_description %} +{% block content %} +
+ +
+

Welcome to OWASP

+

+ Welcome to the Open Worldwide Application Security Project (OWASP) community! OWASP is a global, nonprofit organization focused on improving the security of software through community-led open-source projects, local chapters, education, and collaboration. This page will guide you through everything you need to know to get involved, whether you’re joining a local chapter, contributing to open-source projects, or even starting your own initiative. +

+
+ +
+

What is OWASP?

+

+ OWASP is a community-driven organization dedicated to improving the security of software. Our mission is to make security visible so that individuals and organizations worldwide can make informed decisions about software security risks. +

+
+ +
+

Getting Involved in OWASP

+
+

1. Join a Local Chapter

+

+ OWASP operates local chapters in cities across the globe, offering a great way to connect with like-minded professionals and engage with the community in person or virtually. +

+
    +
  • + Find Your Chapter: Visit the OWASP Chapters page to locate a chapter near you. +
  • +
  • Chapter Activities: Chapters host events such as meetups, workshops, conferences, and training sessions.
  • +
  • + Start Your Own Chapter: If there isn’t a chapter in your area, you can apply to start one. OWASP provides all the resources and support needed to help you build a local community. +
  • +
+
+
+

2. Become a Member

+

+ Joining OWASP as a member provides access to exclusive benefits while supporting the community’s mission. +

+
    +
  • + Membership Benefits: Discounts on conferences, voting rights for the OWASP Board elections, and access to special resources. +
  • +
  • + How to Join: Membership options include individual, student, and corporate levels. Visit OWASP Membership for more details. +
  • +
+
+
+

3. Contribute to OWASP

+

+ OWASP thrives because of its contributors! Whether you’re a developer, writer, designer, or security expert, there’s a role for you. +

+
    +
  • Ways to Contribute:
  • +
      +
    • Write documentation.
    • +
    • Develop and test tools.
    • +
    • Translate projects into other languages.
    • +
    • Provide feedback and reviews for active projects.
    • +
    +
  • + How to Start: Explore the OWASP GitHub repository to find active projects where you can contribute. +
  • +
+
+
+ +
+

Open Source Projects

+

+ OWASP is best known for its community-driven projects. These projects range from tools and frameworks to guidelines and methodologies. +

+ +
+ +
+

Starting Your Own Project

+

+ OWASP encourages innovation and supports individuals looking to start their own projects. +

+
    +
  • How to Propose a Project:
  • +
      +
    • + Review the OWASP Project Process to understand the steps for creating a new project. +
    • +
    • Submit a project proposal that outlines the goals, scope, and resources needed.
    • +
    +
  • Project Support:
  • +
      +
    • Access OWASP’s vast network for mentorship, funding, and collaboration.
    • +
    • Showcase your project at OWASP global events to reach a wider audience.
    • +
    +
+
+ +
+

Stay Connected

+
    +
  • Mailing Lists: Sign up for OWASP newsletters to stay updated on global and local events.
  • +
  • + Events: Attend OWASP Global AppSec conferences to network and learn from industry leaders. +
  • +
  • + Social Media: Follow OWASP on Twitter, LinkedIn, and other platforms. +
  • +
+
+ +
+

Conclusion

+

+ OWASP thrives on its community’s energy and contributions. Whether you’re here to learn, connect, or innovate, there’s a place for you at OWASP. Take the first step today—join a chapter, contribute to a project, or start your own initiative. Together, we can make software security visible and accessible for everyone. +

+

+ For additional questions or guidance, visit the OWASP website or reach out to a chapter leader near you. +

+
+
+{% endblock content %} From 399de4a23f12b66b0d89b6bd937613e00edfdeec Mon Sep 17 00:00:00 2001 From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:58:47 +0530 Subject: [PATCH 2/2] Challenges and leaderboards (#3127) --- blt/urls.py | 6 + website/apps.py | 3 +- website/challenge_signals.py | 199 ++++++++++++++++++ website/{signals.py => feed_signals.py} | 7 +- website/migrations/0173_challenge.py | 56 +++++ .../0174_add_single_user_challenges.py | 48 +++++ .../migrations/0175_add_team_challenges.py | 48 +++++ .../migrations/0176_merge_20241219_0544.py | 12 ++ .../0177_alter_challenge_team_participants.py | 17 ++ .../migrations/0178_merge_20241229_1948.py | 12 ++ ...rge_20241229_1948_0179_contributorstats.py | 12 ++ .../migrations/0183_merge_20250124_0618.py | 12 ++ ...rge_20250124_0618_0183_slackbotactivity.py | 12 ++ website/models.py | 27 +++ website/templates/includes/sidenav.html | 29 ++- website/templates/team_challenges.html | 153 ++++++++++++++ website/templates/team_leaderboard.html | 134 ++++++++++++ website/templates/user_challenges.html | 136 ++++++++++++ website/views/issue.py | 2 + website/views/teams.py | 105 ++++++--- website/views/user.py | 24 +++ 21 files changed, 1019 insertions(+), 35 deletions(-) create mode 100644 website/challenge_signals.py rename website/{signals.py => feed_signals.py} (94%) create mode 100644 website/migrations/0173_challenge.py create mode 100644 website/migrations/0174_add_single_user_challenges.py create mode 100644 website/migrations/0175_add_team_challenges.py create mode 100644 website/migrations/0176_merge_20241219_0544.py create mode 100644 website/migrations/0177_alter_challenge_team_participants.py create mode 100644 website/migrations/0178_merge_20241229_1948.py create mode 100644 website/migrations/0180_merge_0178_merge_20241229_1948_0179_contributorstats.py create mode 100644 website/migrations/0183_merge_20250124_0618.py create mode 100644 website/migrations/0184_merge_0183_merge_20250124_0618_0183_slackbotactivity.py create mode 100644 website/templates/team_challenges.html create mode 100644 website/templates/team_leaderboard.html create mode 100644 website/templates/user_challenges.html diff --git a/blt/urls.py b/blt/urls.py index 31cd29345..3fd33c56e 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -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, @@ -197,6 +199,7 @@ GlobalLeaderboardView, InviteCreate, SpecificMonthLeaderboardView, + UserChallengeListView, UserDeleteView, UserProfileDetailsView, UserProfileDetailView, @@ -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//", 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"), diff --git a/website/apps.py b/website/apps.py index 864790176..14ee23a24 100644 --- a/website/apps.py +++ b/website/apps.py @@ -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 diff --git a/website/challenge_signals.py b/website/challenge_signals.py new file mode 100644 index 000000000..e2374e0e9 --- /dev/null +++ b/website/challenge_signals.py @@ -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 diff --git a/website/signals.py b/website/feed_signals.py similarity index 94% rename from website/signals.py rename to website/feed_signals.py index b8995c7ac..821d2f80e 100644 --- a/website/signals.py +++ b/website/feed_signals.py @@ -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") @@ -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 ) diff --git a/website/migrations/0173_challenge.py b/website/migrations/0173_challenge.py new file mode 100644 index 000000000..9d960d612 --- /dev/null +++ b/website/migrations/0173_challenge.py @@ -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"), + ), + ], + ), + ] diff --git a/website/migrations/0174_add_single_user_challenges.py b/website/migrations/0174_add_single_user_challenges.py new file mode 100644 index 000000000..4da724f7e --- /dev/null +++ b/website/migrations/0174_add_single_user_challenges.py @@ -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), + ] diff --git a/website/migrations/0175_add_team_challenges.py b/website/migrations/0175_add_team_challenges.py new file mode 100644 index 000000000..5ca74b983 --- /dev/null +++ b/website/migrations/0175_add_team_challenges.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.3 on 2024-12-18 13:12 + +from django.db import migrations + + +def add_team_challenges(apps, schema_editor): + Challenge = apps.get_model("website", "Challenge") + + # Define the team challenges + team_challenges = [ + { + "title": "Report 10 IPs", + "description": "Report 10 different suspicious IPs as a team to complete this challenge.", + "challenge_type": "team", + "points": 1, + }, + { + "title": "Report 10 Issues", + "description": "Report 10 unique issues as a team to complete this challenge.", + "challenge_type": "team", + "points": 1, + }, + { + "title": "All Members Sign in for 5 Days", + "description": "Ensure all team members sign in for 5 consecutive days to complete this challenge.", + "challenge_type": "team", + "points": 1, + }, + ] + + # Insert challenges into the Challenge model + for challenge in team_challenges: + Challenge.objects.create( + title=challenge["title"], + description=challenge["description"], + challenge_type=challenge["challenge_type"], + points=challenge["points"], + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0174_add_single_user_challenges"), + ] + + operations = [ + migrations.RunPython(add_team_challenges), + ] diff --git a/website/migrations/0176_merge_20241219_0544.py b/website/migrations/0176_merge_20241219_0544.py new file mode 100644 index 000000000..39a997785 --- /dev/null +++ b/website/migrations/0176_merge_20241219_0544.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.3 on 2024-12-19 05:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0173_remove_company_admin_remove_company_integrations_and_more"), + ("website", "0175_add_team_challenges"), + ] + + operations = [] diff --git a/website/migrations/0177_alter_challenge_team_participants.py b/website/migrations/0177_alter_challenge_team_participants.py new file mode 100644 index 000000000..b06da6438 --- /dev/null +++ b/website/migrations/0177_alter_challenge_team_participants.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-12-19 06:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0176_merge_20241219_0544"), + ] + + operations = [ + migrations.AlterField( + model_name="challenge", + name="team_participants", + field=models.ManyToManyField(blank=True, related_name="team_challenges", to="website.organization"), + ), + ] diff --git a/website/migrations/0178_merge_20241229_1948.py b/website/migrations/0178_merge_20241229_1948.py new file mode 100644 index 000000000..f930450b9 --- /dev/null +++ b/website/migrations/0178_merge_20241229_1948.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.3 on 2024-12-29 19:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0176_repo_contributor_repo_contributor_count_and_more"), + ("website", "0177_alter_challenge_team_participants"), + ] + + operations = [] diff --git a/website/migrations/0180_merge_0178_merge_20241229_1948_0179_contributorstats.py b/website/migrations/0180_merge_0178_merge_20241229_1948_0179_contributorstats.py new file mode 100644 index 000000000..30025a497 --- /dev/null +++ b/website/migrations/0180_merge_0178_merge_20241229_1948_0179_contributorstats.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.4 on 2025-01-08 09:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0178_merge_20241229_1948"), + ("website", "0179_contributorstats"), + ] + + operations = [] diff --git a/website/migrations/0183_merge_20250124_0618.py b/website/migrations/0183_merge_20250124_0618.py new file mode 100644 index 000000000..1dec887c1 --- /dev/null +++ b/website/migrations/0183_merge_20250124_0618.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.4 on 2025-01-24 06:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0180_merge_0178_merge_20241229_1948_0179_contributorstats"), + ("website", "0182_project_status"), + ] + + operations = [] diff --git a/website/migrations/0184_merge_0183_merge_20250124_0618_0183_slackbotactivity.py b/website/migrations/0184_merge_0183_merge_20250124_0618_0183_slackbotactivity.py new file mode 100644 index 000000000..2fb3be0e6 --- /dev/null +++ b/website/migrations/0184_merge_0183_merge_20250124_0618_0183_slackbotactivity.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.4 on 2025-01-26 10:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("website", "0183_merge_20250124_0618"), + ("website", "0183_slackbotactivity"), + ] + + operations = [] diff --git a/website/models.py b/website/models.py index b0a9f409c..36b2c9abc 100644 --- a/website/models.py +++ b/website/models.py @@ -531,6 +531,9 @@ class Points(models.Model): modified = models.DateTimeField(auto_now=True) reason = models.TextField(null=True, blank=True) + def __str__(self): + return f"{self.user.username} - {self.score} points" + class InviteFriend(models.Model): sender = models.ForeignKey(User, related_name="sent_invites", on_delete=models.CASCADE) @@ -1326,3 +1329,27 @@ class Meta: def __str__(self): return f"{self.get_activity_type_display()} in {self.workspace_name} at {self.created}" + + +class Challenge(models.Model): + CHALLENGE_TYPE_CHOICES = [ + ("single", "Single User"), + ("team", "Team"), + ] + + title = models.CharField(max_length=255) + description = models.TextField() + challenge_type = models.CharField(max_length=10, choices=CHALLENGE_TYPE_CHOICES, default="single") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + participants = models.ManyToManyField(User, related_name="user_challenges", blank=True) # For single users + team_participants = models.ManyToManyField( + Organization, related_name="team_challenges", blank=True + ) # For team challenges + points = models.IntegerField(default=0) # Points 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) + + def __str__(self): + return self.title diff --git a/website/templates/includes/sidenav.html b/website/templates/includes/sidenav.html index 2b97f5bea..ce9695a6d 100644 --- a/website/templates/includes/sidenav.html +++ b/website/templates/includes/sidenav.html @@ -190,6 +190,15 @@ diff --git a/website/templates/team_challenges.html b/website/templates/team_challenges.html new file mode 100644 index 000000000..337028077 --- /dev/null +++ b/website/templates/team_challenges.html @@ -0,0 +1,153 @@ +{% extends "base.html" %} +{% block content %} + {% include "includes/sidenav.html" %} + +
+

Team Challenges

+ {% if team_challenges %} +
+ {% for challenge in team_challenges %} +
+
+ {{ challenge.title }} + {{ challenge.points }} pts + +
+
+

{{ challenge.description }}

+
+ +
+ {{ user_team.name }} +
+ +
+
+ {{ challenge.progress|floatformat:0 }}% +
+
+ {% endfor %} +
+ {% else %} +

No team challenges available.

+ {% endif %} +
+ +{% endblock content %} diff --git a/website/templates/team_leaderboard.html b/website/templates/team_leaderboard.html new file mode 100644 index 000000000..3062e4da3 --- /dev/null +++ b/website/templates/team_leaderboard.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} +{% load static %} +{% block title %} + Team Leaderboard +{% endblock title %} +{% block description %} + Check out the top performing teams on the leaderboard. View their scores, names, and rankings. +{% endblock description %} +{% block keywords %} + Team Leaderboard, Top Teams, Scores, Rankings, Team Performance +{% endblock keywords %} +{% block og_title %} + Team Leaderboard - Top Performing Teams +{% endblock og_title %} +{% block og_description %} + Explore the leaderboard to see the top performing teams and their rankings. +{% endblock og_description %} +{% load gravatar %} +{% block style %} + +{% endblock style %} +{% block content %} + {% include "includes/sidenav.html" %} +
+
+

Global Teams Leaderboard

+
+
+
+
+
+ {% if not leaderboard %} +

No data available for teams yet

+ {% else %} + {% for team, points in leaderboard %} +
+ {% if team.logo %} + {{ team.name }} + {% else %} + {{ team.name }} + {% endif %} + {{ team.name }} + {{ points }} Points + {% if team.rank %} + {{ team.rank }} Rank + {% endif %} +
+ {% endfor %} + {% endif %} +
+
+
+{% endblock content %} diff --git a/website/templates/user_challenges.html b/website/templates/user_challenges.html new file mode 100644 index 000000000..50b5d264c --- /dev/null +++ b/website/templates/user_challenges.html @@ -0,0 +1,136 @@ +{% extends "base.html" %} +{% block content %} + {% include "includes/sidenav.html" %} + +
+

Single User Challenges

+ {% if challenges %} +
+ {% for challenge in challenges %} +
+
+ {{ challenge.title }} + {{ challenge.points }} pts + +
+
+

{{ challenge.description }}

+
+ +
+
+ {{ challenge.progress|floatformat:0 }}% +
+
+ {% endfor %} +
+ {% else %} +

No challenges available.

+ {% endif %} +
+ +{% endblock content %} diff --git a/website/views/issue.py b/website/views/issue.py index 88ab24fed..132ea7df6 100644 --- a/website/views/issue.py +++ b/website/views/issue.py @@ -1431,6 +1431,7 @@ def comment_on_content(request, content_pk): content_type = request.POST.get("content_type") content_type_obj = ContentType.objects.get(model=content_type) content = content_type_obj.get_object_for_this_type(pk=content_pk) + VALID_CONTENT_TYPES = ["issue", "post"] if request.method == "POST" and isinstance(request.user, User): @@ -1452,6 +1453,7 @@ def comment_on_content(request, content_pk): if parent_comment is None: messages.error(request, "Parent comment doesn't exist.") + return redirect("home") Comment.objects.create( diff --git a/website/views/teams.py b/website/views/teams.py index 66e927742..70bbafbe4 100644 --- a/website/views/teams.py +++ b/website/views/teams.py @@ -9,7 +9,7 @@ # Create your views here. from django.views.generic import TemplateView -from website.models import JoinRequest, Organization +from website.models import Challenge, JoinRequest, Organization class TeamOverview(TemplateView): @@ -91,11 +91,12 @@ def join_requests(request): if request.method == "POST": team_id = request.POST.get("team_id") team = Organization.objects.get(id=team_id, type="team") - user_profile = request.user.userprofile - user_profile.team = team - user_profile.save() - team.managers.add(request.user) - JoinRequest.objects.filter(user=request.user, team=team).delete() + if request.user.is_authenticated: + user_profile = request.user.userprofile + user_profile.team = team + user_profile.save() + team.managers.add(request.user) + JoinRequest.objects.filter(user=request.user, team=team).delete() return redirect("team_overview") return render(request, "join_requests.html", {"join_requests": join_requests}) @@ -139,34 +140,36 @@ def send_join_request(team, requesting_user, target_username): @login_required def delete_team(request): - user_profile = request.user.userprofile - if user_profile.team and user_profile.team.admin == request.user: - team = user_profile.team - team.managers.clear() - team.delete() - user_profile.team = None - user_profile.save() + if request.user.is_authenticated: + user_profile = request.user.userprofile + if user_profile.team and user_profile.team.admin == request.user: + team = user_profile.team + team.managers.clear() + team.delete() + user_profile.team = None + user_profile.save() return redirect("team_overview") @login_required def leave_team(request): - user_profile = request.user.userprofile - if user_profile.team: - team = user_profile.team - if team.admin == request.user: - managers = team.managers.all() - if managers.exists(): - new_admin = managers.first() - team.managers.remove(new_admin) - team.admin = new_admin - team.save() + if request.user.is_authenticated: + user_profile = request.user.userprofile + if user_profile.team: + team = user_profile.team + if team.admin == request.user: + managers = team.managers.all() + if managers.exists(): + new_admin = managers.first() + team.managers.remove(new_admin) + team.admin = new_admin + team.save() + else: + team.delete() else: - team.delete() - else: - team.managers.remove(request.user) - user_profile.team = None - user_profile.save() + team.managers.remove(request.user) + user_profile.team = None + user_profile.save() return redirect("team_overview") @@ -204,3 +207,49 @@ def kick_member(request): except json.JSONDecodeError: return JsonResponse({"success": False, "error": "Invalid JSON data"}) return JsonResponse({"success": False, "error": "Invalid request method"}) + + +class TeamChallenges(TemplateView): + """View for displaying all team challenges and their progress.""" + + def get(self, request): + # Get all team challenges + team_challenges = Challenge.objects.filter(challenge_type="team") + if request.user.is_authenticated: + user_profile = request.user.userprofile # Get the user's profile + + # Check if the user belongs to a team + if user_profile.team: + user_team = user_profile.team # Get the user's team + + for challenge in team_challenges: + # Check if the team is a participant in this challenge + if user_team in challenge.team_participants.all(): + # Progress is already stored in the Challenge model + challenge.progress = challenge.progress + else: + # Team is not a participant, set progress to 0 + challenge.progress = 0 + else: + # If the user is not part of a team, set progress to 0 for all challenges + for challenge in team_challenges: + challenge.progress = 0 + + # Render the team challenges template + return render(request, "team_challenges.html", {"team_challenges": team_challenges}) + + +class TeamLeaderboard(TemplateView): + """View to display the team leaderboard based on total points.""" + + def get(self, request): + teams = Organization.objects.all() + leaderboard = [] + for team in teams: + team_points = team.team_points + leaderboard.append((team, team_points)) + + # Sort by points in descending order + leaderboard.sort(key=lambda x: x[1], reverse=True) + + return render(request, "team_leaderboard.html", {"leaderboard": leaderboard}) diff --git a/website/views/user.py b/website/views/user.py index 2e5d9c6ee..d894e785f 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -33,6 +33,7 @@ from website.models import ( IP, Badge, + Challenge, Domain, Hunt, InviteFriend, @@ -994,3 +995,26 @@ def assign_github_badge(user, action_title): # computed_signature = f"sha256={computed_hmac.hexdigest()}" # return hmac.compare_digest(computed_signature, signature) + + +@method_decorator(login_required, name="dispatch") +class UserChallengeListView(View): + """View to display all challenges and handle updates inline.""" + + def get(self, request): + challenges = Challenge.objects.filter(challenge_type="single") # All single-user challenges + user_challenges = challenges.filter(participants=request.user) # Challenges the user is participating in + + for challenge in challenges: + if challenge in user_challenges: + # If the user is participating, show their progress + challenge.progress = challenge.progress + else: + # If the user is not participating, set progress to 0 + challenge.progress = 0 + + return render( + request, + "user_challenges.html", + {"challenges": challenges, "user_challenges": user_challenges}, + )