Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
af653e9
Add daily challenges feature
Pritz395 Dec 10, 2025
b9dbf4e
fix: Add required attribute to blockers select and server-side valida…
Pritz395 Dec 10, 2025
0f516af
fix: Use select_for_update() to prevent race condition in mark_completed
Pritz395 Dec 10, 2025
06b885f
fix: Add user-facing error messages and prevent XSS vulnerabilities
Pritz395 Dec 10, 2025
c1fc5ef
fix: Exclude streak_milestone from random daily challenge assignment
Pritz395 Dec 10, 2025
1ad7d0c
feat: Add 3 new daily challenge types and ensure all are truly daily-…
Pritz395 Dec 10, 2025
42ba389
fix: Use user's timezone for Early Bird challenge validation
Pritz395 Dec 10, 2025
10f40d1
fix: Prevent multiple check-in submissions per day
Pritz395 Dec 10, 2025
6b81859
feat: Disable check-in form when already submitted today
Pritz395 Dec 10, 2025
61245e5
chore: Add management command for generating daily challenges
Pritz395 Dec 12, 2025
856a970
style: Fix import order in daily_challenge_service.py
Pritz395 Dec 11, 2025
e282bd6
fix: timezone-aware fallback and remove redundant index
Pritz395 Dec 11, 2025
0581484
fix: migration duplicate handling, blockers validation, query optimiz…
Pritz395 Dec 11, 2025
f94ee21
feat: Add data migration to create initial daily challenge records
Pritz395 Dec 11, 2025
7f44bf1
refactor: Combine all daily challenge migrations into single migratio…
Pritz395 Dec 11, 2025
32bc6ab
fix: Replace JSON string booleans with actual booleans, add mood vali…
Pritz395 Dec 12, 2025
0ae66f4
fix: Resolve merge conflict markers in daily_challenge_service.py
Pritz395 Dec 12, 2025
7f7be36
fix: Address CodeRabbit issues - migration field name, reverse migrat…
Pritz395 Dec 12, 2025
bb73edc
security: Redact PII/secrets from debug logs - log only metadata (len…
Pritz395 Dec 12, 2025
e0fc425
fix: Update frontend to check boolean success value instead of string…
Pritz395 Dec 12, 2025
8570a8f
style: Fix pre-commit linting and formatting issues - remove function…
Pritz395 Dec 12, 2025
72a8b6e
fix: Exclude streak_milestone from management command to match automa…
Pritz395 Dec 12, 2025
a5bbf6f
style: Apply ruff formatting fix
Pritz395 Dec 12, 2025
58e4053
fix: Check date field instead of created timestamp for new challenge …
Pritz395 Dec 12, 2025
e9c3242
fix: Address all CodeRabbit and CI issues
Pritz395 Dec 12, 2025
a5731b1
fix: Create UserProfile if missing in early check-in validation
Pritz395 Dec 12, 2025
3d2289c
fix: Address all peer review recommendations
Pritz395 Dec 12, 2025
c088632
style: Apply formatting fixes to daily challenge files only
Pritz395 Dec 12, 2025
be848b0
fix: Remove red classes when showing 'Available now!' for empty next …
Pritz395 Dec 12, 2025
043ce6d
fix: Restore green styling in success message card after error state
Pritz395 Dec 12, 2025
5dc56f0
style: Apply djlint formatting fix for HTML template
Pritz395 Dec 12, 2025
4ab1d71
fix: Initialize blockers textarea state on page load and prevent dupl…
Pritz395 Dec 12, 2025
7c5327c
fix: Add null check guard for submit button to prevent JS exceptions
Pritz395 Dec 12, 2025
9a6205c
fix: Check next_challenge_at to enforce 24-hour cooldown period
Pritz395 Dec 13, 2025
3a50ec9
Merge branch 'main' into feature/daily-challenges
Pritz395 Dec 13, 2025
f065be5
fix: Prevent information exposure through exception messages
Pritz395 Dec 13, 2025
588ba4c
fix: Address peer review issues - atomic operations, XSS protection, …
Pritz395 Dec 13, 2025
e855bd7
fix: Set next_challenge_at for completed challenges to enforce cooldown
Pritz395 Dec 13, 2025
9c6a988
fix: Treat empty/whitespace blockers field as 'no blockers'
Pritz395 Dec 13, 2025
3c3f2f3
fix: Scope challenge query to today's date
Pritz395 Dec 13, 2025
d516dcf
chore: Trigger CI workflows
Pritz395 Dec 13, 2025
47a5c47
chore: Re-trigger CI workflows
Pritz395 Dec 13, 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
127 changes: 127 additions & 0 deletions website/management/commands/generate_daily_challenges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Management command: website/management/commands/generate_daily_challenges.py

import logging
import random
from datetime import date

from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.utils import timezone

from website.models import DailyChallenge, UserDailyChallenge

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Generate daily challenges for all active users"

def add_arguments(self, parser):
parser.add_argument(
"--date",
type=str,
help="Date to generate challenges for (YYYY-MM-DD). Defaults to today.",
)
parser.add_argument(
"--force",
action="store_true",
help="Force regeneration even if challenges already exist for the date",
)

def handle(self, *args, **options):
# Get target date
if options["date"]:
try:
target_date = date.fromisoformat(options["date"])
except ValueError:
self.stdout.write(
self.style.ERROR(
f"Invalid date format: {options['date']}. Use YYYY-MM-DD.",
),
)
return
else:
target_date = timezone.now().date()

# Get active challenge types, excluding streak_milestone
# Streak milestones should be handled separately as they only complete on specific days
# Only include challenges that can be completed on any day
daily_completable_types = [
"early_checkin",
"positive_mood",
"complete_all_fields",
"no_blockers",
"detailed_reporter",
"goal_achiever",
"detailed_planner",
]
challenge_list = list(DailyChallenge.objects.filter(is_active=True, challenge_type__in=daily_completable_types))
if not challenge_list:
self.stdout.write(
self.style.WARNING(
"No active daily-completable challenge types found. Create challenge types in admin first.",
),
)
return

# Get all active users
users_list = list(User.objects.filter(is_active=True))
if not users_list:
self.stdout.write(self.style.WARNING("No active users found."))
return
created_count = 0
updated_count = 0
skipped_count = 0
error_count = 0

for user in users_list:
# Check if challenge already exists (for skip logic only)
existing = UserDailyChallenge.objects.filter(
user=user,
challenge_date=target_date,
).first()

if existing and not options["force"]:
skipped_count += 1
continue

# Randomly select a challenge type for this user
selected_challenge = random.choice(challenge_list)

try:
# Use update_or_create for atomic operation to avoid race conditions
# This ensures the check-and-update/create is atomic, preventing partial state
user_challenge, created = UserDailyChallenge.objects.update_or_create(
user=user,
challenge_date=target_date,
defaults={
"challenge": selected_challenge,
"status": "assigned",
"completed_at": None,
"points_awarded": 0,
"next_challenge_at": None,
},
)
if created:
created_count += 1
else:
updated_count += 1
except Exception as e:
error_count += 1
logger.error(
f"Error creating/updating challenge for user {user.username}: {e}",
exc_info=True,
)
self.stdout.write(
self.style.ERROR(
f"Error for user {user.username}: {str(e)}",
),
)

self.stdout.write(
self.style.SUCCESS(
f"Successfully created {created_count} challenges, "
f"updated {updated_count} challenges. "
f"Skipped: {skipped_count}, Errors: {error_count}",
),
)
Loading
Loading