From a00d2abe0d5a72c02053139e3f212c1e93f6238f Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 4 Feb 2025 10:37:15 -0500 Subject: [PATCH] feat: Add achievements app with models, admin, and management command for seeding data --- backend/server/achievements/__init__.py | 0 backend/server/achievements/admin.py | 9 +++ backend/server/achievements/apps.py | 6 ++ .../achievements/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/achievement-seed.py | 66 +++++++++++++++++++ .../achievements/migrations/0001_initial.py | 39 +++++++++++ .../achievements/migrations/__init__.py | 0 backend/server/achievements/models.py | 33 ++++++++++ backend/server/achievements/tests.py | 3 + backend/server/achievements/views.py | 3 + backend/server/main/settings.py | 1 + 12 files changed, 160 insertions(+) create mode 100644 backend/server/achievements/__init__.py create mode 100644 backend/server/achievements/admin.py create mode 100644 backend/server/achievements/apps.py create mode 100644 backend/server/achievements/management/__init__.py create mode 100644 backend/server/achievements/management/commands/__init__.py create mode 100644 backend/server/achievements/management/commands/achievement-seed.py create mode 100644 backend/server/achievements/migrations/0001_initial.py create mode 100644 backend/server/achievements/migrations/__init__.py create mode 100644 backend/server/achievements/models.py create mode 100644 backend/server/achievements/tests.py create mode 100644 backend/server/achievements/views.py diff --git a/backend/server/achievements/__init__.py b/backend/server/achievements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/admin.py b/backend/server/achievements/admin.py new file mode 100644 index 0000000..af087ce --- /dev/null +++ b/backend/server/achievements/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from allauth.account.decorators import secure_admin_login +from achievements.models import Achievement, UserAchievement + +admin.autodiscover() +admin.site.login = secure_admin_login(admin.site.login) + +admin.site.register(Achievement) +admin.site.register(UserAchievement) \ No newline at end of file diff --git a/backend/server/achievements/apps.py b/backend/server/achievements/apps.py new file mode 100644 index 0000000..2a635e2 --- /dev/null +++ b/backend/server/achievements/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AchievementsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'achievements' diff --git a/backend/server/achievements/management/__init__.py b/backend/server/achievements/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/management/commands/__init__.py b/backend/server/achievements/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/management/commands/achievement-seed.py b/backend/server/achievements/management/commands/achievement-seed.py new file mode 100644 index 0000000..7713e88 --- /dev/null +++ b/backend/server/achievements/management/commands/achievement-seed.py @@ -0,0 +1,66 @@ +import json +from django.core.management.base import BaseCommand +from achievements.models import Achievement + +US_STATE_CODES = [ + 'US-AL', 'US-AK', 'US-AZ', 'US-AR', 'US-CA', 'US-CO', 'US-CT', 'US-DE', + 'US-FL', 'US-GA', 'US-HI', 'US-ID', 'US-IL', 'US-IN', 'US-IA', 'US-KS', + 'US-KY', 'US-LA', 'US-ME', 'US-MD', 'US-MA', 'US-MI', 'US-MN', 'US-MS', + 'US-MO', 'US-MT', 'US-NE', 'US-NV', 'US-NH', 'US-NJ', 'US-NM', 'US-NY', + 'US-NC', 'US-ND', 'US-OH', 'US-OK', 'US-OR', 'US-PA', 'US-RI', 'US-SC', + 'US-SD', 'US-TN', 'US-TX', 'US-UT', 'US-VT', 'US-VA', 'US-WA', 'US-WV', + 'US-WI', 'US-WY' +] + +ACHIEVEMENTS = [ + { + "name": "First Adventure", + "key": "achievements.first_adventure", + "type": "adventure_count", + "description": "Log your first adventure!", + "condition": {"type": "adventure_count", "value": 1}, + }, + { + "name": "Explorer", + "key": "achievements.explorer", + "type": "adventure_count", + "description": "Log 10 adventures.", + "condition": {"type": "adventure_count", "value": 10}, + }, + { + "name": "Globetrotter", + "key": "achievements.globetrotter", + "type": "country_count", + "description": "Visit 5 different countries.", + "condition": {"type": "country_count", "value": 5}, + }, + { + "name": "American Dream", + "key": "achievements.american_dream", + "type": "country_count", + "description": "Visit all 50 states in the USA.", + "condition": {"type": "country_count", "items": US_STATE_CODES}, + } +] + + + + +class Command(BaseCommand): + help = "Seeds the database with predefined achievements" + + def handle(self, *args, **kwargs): + for achievement_data in ACHIEVEMENTS: + achievement, created = Achievement.objects.update_or_create( + name=achievement_data["name"], + defaults={ + "description": achievement_data["description"], + "condition": json.dumps(achievement_data["condition"]), + "type": achievement_data["type"], + "key": achievement_data["key"], + }, + ) + if created: + self.stdout.write(self.style.SUCCESS(f"✅ Created: {achievement.name}")) + else: + self.stdout.write(self.style.WARNING(f"🔄 Updated: {achievement.name}")) diff --git a/backend/server/achievements/migrations/0001_initial.py b/backend/server/achievements/migrations/0001_initial.py new file mode 100644 index 0000000..e38e509 --- /dev/null +++ b/backend/server/achievements/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.8 on 2025-02-04 04:34 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Achievement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('description', models.TextField()), + ('icon', models.ImageField(blank=True, null=True, upload_to='achievements/')), + ('condition', models.JSONField()), + ], + ), + migrations.CreateModel( + name='UserAchievement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('earned_at', models.DateTimeField(auto_now_add=True)), + ('achievement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='achievements.achievement')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'achievement')}, + }, + ), + ] diff --git a/backend/server/achievements/migrations/__init__.py b/backend/server/achievements/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/achievements/models.py b/backend/server/achievements/models.py new file mode 100644 index 0000000..8659bc2 --- /dev/null +++ b/backend/server/achievements/models.py @@ -0,0 +1,33 @@ +from django.db import models +from django.contrib.auth import get_user_model + +User = get_user_model() + +VALID_ACHIEVEMENT_TYPES = [ + "adventure_count", + "country_count", +] + +class Achievement(models.Model): + """Stores all possible achievements""" + name = models.CharField(max_length=255, unique=True) + key = models.CharField(max_length=255, unique=True) # Used for frontend lookups, e.g. "achievements.first_adventure" + type = models.CharField(max_length=255) # adventure_count, country_count, etc. + description = models.TextField() + icon = models.ImageField(upload_to="achievements/", null=True, blank=True) + condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10} + + def __str__(self): + return self.name + +class UserAchievement(models.Model): + """Tracks which achievements a user has earned""" + user = models.ForeignKey(User, on_delete=models.CASCADE) + achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE) + earned_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("user", "achievement") # Prevent duplicates + + def __str__(self): + return f"{self.user.username} - {self.achievement.name}" diff --git a/backend/server/achievements/tests.py b/backend/server/achievements/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/server/achievements/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/server/achievements/views.py b/backend/server/achievements/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/server/achievements/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 25f29ba..5549c0d 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -61,6 +61,7 @@ INSTALLED_APPS = ( 'users', 'integrations', 'django.contrib.gis', + 'achievements', # 'widget_tweaks', # 'slippers',