init backend
- SHA
9c38f5f501f4f391f875753fde2c716bddd510ff- Tree
0ddeb57
9c38f5f
9c38f5f501f4f391f875753fde2c716bddd510ff0ddeb57| Status | File | + | - |
|---|---|---|---|
| A |
.gitignore
|
51 | 0 |
| A |
.tool-versions
|
1 | 0 |
| A |
backend/.env.example
|
13 | 0 |
| A |
backend/apps/__init__.py
|
0 | 0 |
| A |
backend/apps/trees/__init__.py
|
0 | 0 |
| A |
backend/apps/trees/admin.py
|
72 | 0 |
| A |
backend/apps/trees/apps.py
|
6 | 0 |
| A |
backend/apps/trees/management/__init__.py
|
0 | 0 |
| A |
backend/apps/trees/management/commands/__init__.py
|
0 | 0 |
| A |
backend/apps/trees/management/commands/populate_trees.py
|
51 | 0 |
| A |
backend/apps/trees/migrations/0001_initial.py
|
105 | 0 |
| A |
backend/apps/trees/migrations/__init__.py
|
0 | 0 |
| A |
backend/apps/trees/models.py
|
297 | 0 |
| A |
backend/apps/trees/serializers.py
|
47 | 0 |
| A |
backend/apps/trees/tests.py
|
3 | 0 |
| A |
backend/apps/trees/urls.py
|
13 | 0 |
| A |
backend/apps/trees/views.py
|
231 | 0 |
| A |
backend/bashamole/__init__.py
|
0 | 0 |
| A |
backend/bashamole/asgi.py
|
16 | 0 |
| A |
backend/bashamole/settings.py
|
138 | 0 |
| A |
backend/bashamole/urls.py
|
25 | 0 |
| A |
backend/bashamole/wsgi.py
|
16 | 0 |
| A |
backend/manage.py
|
22 | 0 |
| A |
backend/requirements.txt
|
5 | 0 |
| A |
package.json
|
21 | 0 |
.gitignoreadded@@ -0,0 +1,51 @@ | ||
| 1 | +# Python | |
| 2 | +backend/venv/ | |
| 3 | +backend/.molenv/* | |
| 4 | +backend/env/ | |
| 5 | +backend/*.pyc | |
| 6 | +backend/__pycache__/ | |
| 7 | +backend/**/__pycache__/ | |
| 8 | +backend/db.sqlite3 | |
| 9 | +backend/.env | |
| 10 | +backend/*.log | |
| 11 | +backend/.coverage | |
| 12 | +backend/htmlcov/ | |
| 13 | +backend/media/ | |
| 14 | +backend/staticfiles/ | |
| 15 | + | |
| 16 | +# Node | |
| 17 | +frontend/node_modules/ | |
| 18 | +frontend/.next/ | |
| 19 | +frontend/out/ | |
| 20 | +frontend/dist/ | |
| 21 | +frontend/build/ | |
| 22 | +frontend/.env.local | |
| 23 | +frontend/.env.*.local | |
| 24 | +frontend/npm-debug.log* | |
| 25 | +frontend/yarn-debug.log* | |
| 26 | +frontend/yarn-error.log* | |
| 27 | + | |
| 28 | +# IDE | |
| 29 | +.vscode/ | |
| 30 | +.idea/ | |
| 31 | +*.swp | |
| 32 | +*.swo | |
| 33 | +*~ | |
| 34 | + | |
| 35 | +# OS | |
| 36 | +.DS_Store | |
| 37 | +Thumbs.db | |
| 38 | + | |
| 39 | +# Shared | |
| 40 | +shared/node_modules/ | |
| 41 | +shared/dist/ | |
| 42 | + | |
| 43 | +# Logs | |
| 44 | +*.log | |
| 45 | +logs/ | |
| 46 | + | |
| 47 | +# Testing | |
| 48 | +coverage/ | |
| 49 | +.nyc_output/ | |
| 50 | +*.coverage | |
| 51 | +.pytest_cache/ | |
.tool-versionsadded@@ -0,0 +1,1 @@ | ||
| 1 | +nodejs 23.11.0 | |
backend/.env.exampleadded@@ -0,0 +1,13 @@ | ||
| 1 | +# backend/.env.example | |
| 2 | +DEBUG=True | |
| 3 | +SECRET_KEY=your-secret-key-here | |
| 4 | +ALLOWED_HOSTS=localhost,127.0.0.1 | |
| 5 | + | |
| 6 | +# Database (for production) | |
| 7 | +# DATABASE_URL=postgresql://user:password@localhost/bashamole | |
| 8 | + | |
| 9 | +# CORS | |
| 10 | +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173 | |
| 11 | + | |
| 12 | +# API Settings | |
| 13 | +API_PAGE_SIZE=100 | |
backend/apps/__init__.pyaddedbackend/apps/trees/__init__.pyaddedbackend/apps/trees/admin.pyadded@@ -0,0 +1,72 @@ | ||
| 1 | +# apps/trees/admin.py | |
| 2 | +from django.contrib import admin | |
| 3 | +from .models import FileSystemTree, DirectoryNode, GameSession | |
| 4 | + | |
| 5 | + | |
| 6 | +class DirectoryNodeInline(admin.TabularInline): | |
| 7 | + model = DirectoryNode | |
| 8 | + extra = 0 | |
| 9 | + fields = ['name', 'path', 'is_fhs_standard', 'parent'] | |
| 10 | + readonly_fields = ['path'] | |
| 11 | + | |
| 12 | + def has_add_permission(self, request, obj=None): | |
| 13 | + return False | |
| 14 | + | |
| 15 | + | |
| 16 | +@admin.register(FileSystemTree) | |
| 17 | +class FileSystemTreeAdmin(admin.ModelAdmin): | |
| 18 | + list_display = ['name', 'created_at', 'is_completed', 'mole_location', 'player_location'] | |
| 19 | + list_filter = ['is_completed', 'created_at'] | |
| 20 | + readonly_fields = ['created_at', 'completed_at', 'tree_data', 'seed'] | |
| 21 | + | |
| 22 | + fieldsets = ( | |
| 23 | + ('Basic Info', { | |
| 24 | + 'fields': ('name', 'seed', 'created_at') | |
| 25 | + }), | |
| 26 | + ('Game State', { | |
| 27 | + 'fields': ('player_location', 'mole_location', 'is_completed', 'completed_at') | |
| 28 | + }), | |
| 29 | + ('Tree Data', { | |
| 30 | + 'fields': ('tree_data',), | |
| 31 | + 'classes': ('collapse',) | |
| 32 | + }) | |
| 33 | + ) | |
| 34 | + | |
| 35 | + actions = ['regenerate_trees'] | |
| 36 | + | |
| 37 | + def regenerate_trees(self, request, queryset): | |
| 38 | + for tree in queryset: | |
| 39 | + tree.generate_tree() | |
| 40 | + self.message_user(request, f"Regenerated {queryset.count()} trees") | |
| 41 | + regenerate_trees.short_description = "Regenerate selected trees" | |
| 42 | + | |
| 43 | + | |
| 44 | +@admin.register(DirectoryNode) | |
| 45 | +class DirectoryNodeAdmin(admin.ModelAdmin): | |
| 46 | + list_display = ['path', 'name', 'tree', 'is_fhs_standard', 'parent'] | |
| 47 | + list_filter = ['is_fhs_standard', 'tree'] | |
| 48 | + search_fields = ['path', 'name'] | |
| 49 | + raw_id_fields = ['tree', 'parent'] | |
| 50 | + | |
| 51 | + def get_queryset(self, request): | |
| 52 | + return super().get_queryset(request).select_related('tree', 'parent') | |
| 53 | + | |
| 54 | + | |
| 55 | +@admin.register(GameSession) | |
| 56 | +class GameSessionAdmin(admin.ModelAdmin): | |
| 57 | + list_display = ['player_name', 'tree', 'started_at', 'completed_at', 'commands_used', 'time_taken'] | |
| 58 | + list_filter = ['completed_at', 'started_at'] | |
| 59 | + readonly_fields = ['started_at', 'completed_at', 'time_taken', 'command_history'] | |
| 60 | + | |
| 61 | + fieldsets = ( | |
| 62 | + ('Player Info', { | |
| 63 | + 'fields': ('player_name', 'tree') | |
| 64 | + }), | |
| 65 | + ('Game Stats', { | |
| 66 | + 'fields': ('started_at', 'completed_at', 'time_taken', 'commands_used', 'directories_visited') | |
| 67 | + }), | |
| 68 | + ('Command History', { | |
| 69 | + 'fields': ('command_history',), | |
| 70 | + 'classes': ('collapse',) | |
| 71 | + }) | |
| 72 | + ) | |
backend/apps/trees/apps.pyadded@@ -0,0 +1,6 @@ | ||
| 1 | +from django.apps import AppConfig | |
| 2 | + | |
| 3 | + | |
| 4 | +class TreesConfig(AppConfig): | |
| 5 | + default_auto_field = "django.db.models.BigAutoField" | |
| 6 | + name = "apps.trees" | |
backend/apps/trees/management/__init__.pyaddedbackend/apps/trees/management/commands/__init__.pyaddedbackend/apps/trees/management/commands/populate_trees.pyadded@@ -0,0 +1,51 @@ | ||
| 1 | +# apps/trees/management/commands/populate_trees.py | |
| 2 | +from django.core.management.base import BaseCommand | |
| 3 | +from apps.trees.models import FileSystemTree | |
| 4 | + | |
| 5 | + | |
| 6 | +class Command(BaseCommand): | |
| 7 | + help = 'Create sample filesystem trees for testing' | |
| 8 | + | |
| 9 | + def add_arguments(self, parser): | |
| 10 | + parser.add_argument( | |
| 11 | + '--count', | |
| 12 | + type=int, | |
| 13 | + default=3, | |
| 14 | + help='Number of trees to create' | |
| 15 | + ) | |
| 16 | + | |
| 17 | + def handle(self, *args, **options): | |
| 18 | + count = options['count'] | |
| 19 | + | |
| 20 | + self.stdout.write('Creating sample filesystem trees...') | |
| 21 | + | |
| 22 | + difficulties = [ | |
| 23 | + {'name': 'Beginner Tree', 'max_depth': 3, 'dirs_per_level': 2}, | |
| 24 | + {'name': 'Intermediate Tree', 'max_depth': 4, 'dirs_per_level': 3}, | |
| 25 | + {'name': 'Advanced Tree', 'max_depth': 5, 'dirs_per_level': 4}, | |
| 26 | + {'name': 'Expert Tree', 'max_depth': 6, 'dirs_per_level': 5}, | |
| 27 | + ] | |
| 28 | + | |
| 29 | + created_count = 0 | |
| 30 | + for i in range(count): | |
| 31 | + difficulty = difficulties[i % len(difficulties)] | |
| 32 | + | |
| 33 | + tree = FileSystemTree.objects.create( | |
| 34 | + name=f"{difficulty['name']} #{i+1}" | |
| 35 | + ) | |
| 36 | + tree.generate_tree( | |
| 37 | + max_depth=difficulty['max_depth'], | |
| 38 | + directories_per_level=difficulty['dirs_per_level'] | |
| 39 | + ) | |
| 40 | + | |
| 41 | + created_count += 1 | |
| 42 | + self.stdout.write( | |
| 43 | + self.style.SUCCESS( | |
| 44 | + f"Created tree: {tree.name} with {tree.nodes.count()} directories" | |
| 45 | + ) | |
| 46 | + ) | |
| 47 | + self.stdout.write(f" Mole hidden at: {tree.mole_location}") | |
| 48 | + | |
| 49 | + self.stdout.write( | |
| 50 | + self.style.SUCCESS(f'Successfully created {created_count} filesystem trees') | |
| 51 | + ) | |
backend/apps/trees/migrations/0001_initial.pyadded@@ -0,0 +1,105 @@ | ||
| 1 | +# Generated by Django 5.2.3 on 2025-06-14 18:26 | |
| 2 | + | |
| 3 | +import django.db.models.deletion | |
| 4 | +from django.db import migrations, models | |
| 5 | + | |
| 6 | + | |
| 7 | +class Migration(migrations.Migration): | |
| 8 | + | |
| 9 | + initial = True | |
| 10 | + | |
| 11 | + dependencies = [] | |
| 12 | + | |
| 13 | + operations = [ | |
| 14 | + migrations.CreateModel( | |
| 15 | + name="FileSystemTree", | |
| 16 | + fields=[ | |
| 17 | + ( | |
| 18 | + "id", | |
| 19 | + models.BigAutoField( | |
| 20 | + auto_created=True, | |
| 21 | + primary_key=True, | |
| 22 | + serialize=False, | |
| 23 | + verbose_name="ID", | |
| 24 | + ), | |
| 25 | + ), | |
| 26 | + ("name", models.CharField(default="FHS Tree", max_length=100)), | |
| 27 | + ("created_at", models.DateTimeField(auto_now_add=True)), | |
| 28 | + ("seed", models.IntegerField(default=0)), | |
| 29 | + ("mole_location", models.CharField(blank=True, max_length=500)), | |
| 30 | + ("player_location", models.CharField(default="/home", max_length=500)), | |
| 31 | + ("is_completed", models.BooleanField(default=False)), | |
| 32 | + ("completed_at", models.DateTimeField(blank=True, null=True)), | |
| 33 | + ("tree_data", models.JSONField(blank=True, null=True)), | |
| 34 | + ], | |
| 35 | + ), | |
| 36 | + migrations.CreateModel( | |
| 37 | + name="GameSession", | |
| 38 | + fields=[ | |
| 39 | + ( | |
| 40 | + "id", | |
| 41 | + models.BigAutoField( | |
| 42 | + auto_created=True, | |
| 43 | + primary_key=True, | |
| 44 | + serialize=False, | |
| 45 | + verbose_name="ID", | |
| 46 | + ), | |
| 47 | + ), | |
| 48 | + ("player_name", models.CharField(default="Anonymous", max_length=100)), | |
| 49 | + ("started_at", models.DateTimeField(auto_now_add=True)), | |
| 50 | + ("completed_at", models.DateTimeField(blank=True, null=True)), | |
| 51 | + ("commands_used", models.IntegerField(default=0)), | |
| 52 | + ("directories_visited", models.IntegerField(default=0)), | |
| 53 | + ("time_taken", models.DurationField(blank=True, null=True)), | |
| 54 | + ("command_history", models.JSONField(default=list)), | |
| 55 | + ( | |
| 56 | + "tree", | |
| 57 | + models.ForeignKey( | |
| 58 | + on_delete=django.db.models.deletion.CASCADE, | |
| 59 | + related_name="sessions", | |
| 60 | + to="trees.filesystemtree", | |
| 61 | + ), | |
| 62 | + ), | |
| 63 | + ], | |
| 64 | + ), | |
| 65 | + migrations.CreateModel( | |
| 66 | + name="DirectoryNode", | |
| 67 | + fields=[ | |
| 68 | + ( | |
| 69 | + "id", | |
| 70 | + models.BigAutoField( | |
| 71 | + auto_created=True, | |
| 72 | + primary_key=True, | |
| 73 | + serialize=False, | |
| 74 | + verbose_name="ID", | |
| 75 | + ), | |
| 76 | + ), | |
| 77 | + ("name", models.CharField(max_length=100)), | |
| 78 | + ("path", models.CharField(db_index=True, max_length=500)), | |
| 79 | + ("is_fhs_standard", models.BooleanField(default=False)), | |
| 80 | + ("description", models.TextField(blank=True)), | |
| 81 | + ( | |
| 82 | + "parent", | |
| 83 | + models.ForeignKey( | |
| 84 | + blank=True, | |
| 85 | + null=True, | |
| 86 | + on_delete=django.db.models.deletion.CASCADE, | |
| 87 | + related_name="children", | |
| 88 | + to="trees.directorynode", | |
| 89 | + ), | |
| 90 | + ), | |
| 91 | + ( | |
| 92 | + "tree", | |
| 93 | + models.ForeignKey( | |
| 94 | + on_delete=django.db.models.deletion.CASCADE, | |
| 95 | + related_name="nodes", | |
| 96 | + to="trees.filesystemtree", | |
| 97 | + ), | |
| 98 | + ), | |
| 99 | + ], | |
| 100 | + options={ | |
| 101 | + "ordering": ["path"], | |
| 102 | + "unique_together": {("tree", "path")}, | |
| 103 | + }, | |
| 104 | + ), | |
| 105 | + ] | |
backend/apps/trees/migrations/__init__.pyaddedbackend/apps/trees/models.pyadded@@ -0,0 +1,297 @@ | ||
| 1 | +# apps/trees/models.py | |
| 2 | +from django.db import models | |
| 3 | +from django.utils import timezone | |
| 4 | +import json | |
| 5 | +import random | |
| 6 | +import string | |
| 7 | + | |
| 8 | +class FileSystemTree(models.Model): | |
| 9 | + """A complete filesystem tree for a game session""" | |
| 10 | + name = models.CharField(max_length=100, default="FHS Tree") | |
| 11 | + created_at = models.DateTimeField(auto_now_add=True) | |
| 12 | + seed = models.IntegerField(default=0) | |
| 13 | + | |
| 14 | + # Game state | |
| 15 | + mole_location = models.CharField(max_length=500, blank=True) # Path to mole | |
| 16 | + player_location = models.CharField(max_length=500, default="/home") | |
| 17 | + is_completed = models.BooleanField(default=False) | |
| 18 | + completed_at = models.DateTimeField(null=True, blank=True) | |
| 19 | + | |
| 20 | + # Cached tree structure | |
| 21 | + tree_data = models.JSONField(null=True, blank=True) | |
| 22 | + | |
| 23 | + def __str__(self): | |
| 24 | + return f"{self.name} - {'Completed' if self.is_completed else 'Active'}" | |
| 25 | + | |
| 26 | + def generate_tree(self, max_depth=5, directories_per_level=3): | |
| 27 | + """Generate a procedural Unix filesystem tree""" | |
| 28 | + if self.seed == 0: | |
| 29 | + self.seed = random.randint(1, 1000000) | |
| 30 | + | |
| 31 | + random.seed(self.seed) | |
| 32 | + | |
| 33 | + # Clear existing nodes | |
| 34 | + self.nodes.all().delete() | |
| 35 | + | |
| 36 | + # Create root | |
| 37 | + root = DirectoryNode.objects.create( | |
| 38 | + tree=self, | |
| 39 | + name="", | |
| 40 | + path="/", | |
| 41 | + parent=None, | |
| 42 | + is_fhs_standard=True, | |
| 43 | + description="Root directory" | |
| 44 | + ) | |
| 45 | + | |
| 46 | + # Create standard FHS directories | |
| 47 | + self._create_fhs_structure(root) | |
| 48 | + | |
| 49 | + # Add procedural directories to some locations | |
| 50 | + self._add_procedural_directories(max_depth, directories_per_level) | |
| 51 | + | |
| 52 | + # Place the mole in a random directory (not in standard FHS locations) | |
| 53 | + self._place_mole() | |
| 54 | + | |
| 55 | + # Cache the tree structure | |
| 56 | + self.cache_tree() | |
| 57 | + self.save() | |
| 58 | + | |
| 59 | + def _create_fhs_structure(self, root): | |
| 60 | + """Create standard FHS directory structure""" | |
| 61 | + fhs_dirs = [ | |
| 62 | + {"name": "bin", "desc": "Essential command binaries"}, | |
| 63 | + {"name": "boot", "desc": "Static files of the boot loader"}, | |
| 64 | + {"name": "dev", "desc": "Device files"}, | |
| 65 | + {"name": "etc", "desc": "Host-specific system configuration"}, | |
| 66 | + {"name": "home", "desc": "User home directories"}, | |
| 67 | + {"name": "lib", "desc": "Essential shared libraries and kernel modules"}, | |
| 68 | + {"name": "media", "desc": "Mount points for removable media"}, | |
| 69 | + {"name": "mnt", "desc": "Mount point for temporarily mounted filesystems"}, | |
| 70 | + {"name": "opt", "desc": "Add-on application software packages"}, | |
| 71 | + {"name": "proc", "desc": "Virtual filesystem for process information"}, | |
| 72 | + {"name": "root", "desc": "Home directory for the root user"}, | |
| 73 | + {"name": "run", "desc": "Data relevant to running processes"}, | |
| 74 | + {"name": "sbin", "desc": "Essential system binaries"}, | |
| 75 | + {"name": "srv", "desc": "Data for services provided by this system"}, | |
| 76 | + {"name": "sys", "desc": "Virtual filesystem for system information"}, | |
| 77 | + {"name": "tmp", "desc": "Temporary files"}, | |
| 78 | + {"name": "usr", "desc": "Secondary hierarchy"}, | |
| 79 | + {"name": "var", "desc": "Variable data"}, | |
| 80 | + ] | |
| 81 | + | |
| 82 | + for dir_info in fhs_dirs: | |
| 83 | + DirectoryNode.objects.create( | |
| 84 | + tree=self, | |
| 85 | + name=dir_info["name"], | |
| 86 | + path=f"/{dir_info['name']}", | |
| 87 | + parent=root, | |
| 88 | + is_fhs_standard=True, | |
| 89 | + description=dir_info["desc"] | |
| 90 | + ) | |
| 91 | + | |
| 92 | + # Create some standard subdirectories | |
| 93 | + usr = DirectoryNode.objects.get(tree=self, path="/usr") | |
| 94 | + for subdir in ["bin", "lib", "local", "share", "src"]: | |
| 95 | + DirectoryNode.objects.create( | |
| 96 | + tree=self, | |
| 97 | + name=subdir, | |
| 98 | + path=f"/usr/{subdir}", | |
| 99 | + parent=usr, | |
| 100 | + is_fhs_standard=True, | |
| 101 | + description=f"User {subdir} directory" | |
| 102 | + ) | |
| 103 | + | |
| 104 | + # Create user directories | |
| 105 | + home = DirectoryNode.objects.get(tree=self, path="/home") | |
| 106 | + for username in ["alice", "bob", "charlie"]: | |
| 107 | + user_home = DirectoryNode.objects.create( | |
| 108 | + tree=self, | |
| 109 | + name=username, | |
| 110 | + path=f"/home/{username}", | |
| 111 | + parent=home, | |
| 112 | + is_fhs_standard=False, | |
| 113 | + description=f"Home directory for {username}" | |
| 114 | + ) | |
| 115 | + | |
| 116 | + # Add some standard user directories | |
| 117 | + for userdir in ["Documents", "Downloads", "Desktop", "Pictures"]: | |
| 118 | + DirectoryNode.objects.create( | |
| 119 | + tree=self, | |
| 120 | + name=userdir, | |
| 121 | + path=f"/home/{username}/{userdir}", | |
| 122 | + parent=user_home, | |
| 123 | + is_fhs_standard=False, | |
| 124 | + description=f"{username}'s {userdir}" | |
| 125 | + ) | |
| 126 | + | |
| 127 | + def _add_procedural_directories(self, max_depth, dirs_per_level): | |
| 128 | + """Add procedurally generated directories to make the tree interesting""" | |
| 129 | + # Common directory names for procedural generation | |
| 130 | + dir_names = [ | |
| 131 | + "projects", "workspace", "temp", "backup", "archive", "data", | |
| 132 | + "config", "logs", "cache", "build", "dist", "assets", | |
| 133 | + "scripts", "tools", "utils", "resources", "public", "private", | |
| 134 | + "old", "new", "test", "prod", "dev", "staging", | |
| 135 | + "alpha", "beta", "gamma", "delta", "epsilon", "zeta", | |
| 136 | + "red", "blue", "green", "yellow", "purple", "orange", | |
| 137 | + "cat", "dog", "fish", "bird", "mouse", "rabbit" | |
| 138 | + ] | |
| 139 | + | |
| 140 | + # Add procedural dirs to certain locations | |
| 141 | + base_paths = [ | |
| 142 | + "/home/alice", "/home/bob", "/home/charlie", | |
| 143 | + "/opt", "/var", "/usr/local" | |
| 144 | + ] | |
| 145 | + | |
| 146 | + for base_path in base_paths: | |
| 147 | + try: | |
| 148 | + base_node = DirectoryNode.objects.get(tree=self, path=base_path) | |
| 149 | + self._generate_subtree(base_node, max_depth-2, dirs_per_level, dir_names) | |
| 150 | + except DirectoryNode.DoesNotExist: | |
| 151 | + continue | |
| 152 | + | |
| 153 | + def _generate_subtree(self, parent, depth, dirs_per_level, name_pool): | |
| 154 | + """Recursively generate random subdirectories""" | |
| 155 | + if depth <= 0: | |
| 156 | + return | |
| 157 | + | |
| 158 | + # Random number of directories at this level | |
| 159 | + num_dirs = random.randint(1, dirs_per_level) | |
| 160 | + used_names = set() | |
| 161 | + | |
| 162 | + for _ in range(num_dirs): | |
| 163 | + # Pick a unique name for this level | |
| 164 | + name = random.choice(name_pool) | |
| 165 | + while name in used_names: | |
| 166 | + name = random.choice(name_pool) | |
| 167 | + used_names.add(name) | |
| 168 | + | |
| 169 | + # Create the directory | |
| 170 | + path = f"{parent.path}/{name}" if parent.path != "/" else f"/{name}" | |
| 171 | + new_dir = DirectoryNode.objects.create( | |
| 172 | + tree=self, | |
| 173 | + name=name, | |
| 174 | + path=path, | |
| 175 | + parent=parent, | |
| 176 | + is_fhs_standard=False, | |
| 177 | + description=f"Procedurally generated directory" | |
| 178 | + ) | |
| 179 | + | |
| 180 | + # Randomly decide whether to create subdirectories | |
| 181 | + if random.random() > 0.3: # 70% chance of subdirectories | |
| 182 | + self._generate_subtree(new_dir, depth-1, dirs_per_level, name_pool) | |
| 183 | + | |
| 184 | + def _place_mole(self): | |
| 185 | + """Place the mole in a random non-FHS directory""" | |
| 186 | + candidates = DirectoryNode.objects.filter( | |
| 187 | + tree=self, | |
| 188 | + is_fhs_standard=False | |
| 189 | + ).exclude(path__in=[ | |
| 190 | + "/home", "/home/alice", "/home/bob", "/home/charlie" | |
| 191 | + ]) | |
| 192 | + | |
| 193 | + if candidates.exists(): | |
| 194 | + mole_dir = random.choice(candidates) | |
| 195 | + self.mole_location = mole_dir.path | |
| 196 | + | |
| 197 | + def cache_tree(self): | |
| 198 | + """Cache the tree structure for efficient retrieval""" | |
| 199 | + def build_tree_dict(node): | |
| 200 | + children = DirectoryNode.objects.filter(parent=node) | |
| 201 | + return { | |
| 202 | + "name": node.name, | |
| 203 | + "path": node.path, | |
| 204 | + "is_fhs": node.is_fhs_standard, | |
| 205 | + "description": node.description, | |
| 206 | + "has_mole": node.path == self.mole_location, | |
| 207 | + "children": [build_tree_dict(child) for child in children] | |
| 208 | + } | |
| 209 | + | |
| 210 | + root = DirectoryNode.objects.get(tree=self, path="/") | |
| 211 | + self.tree_data = build_tree_dict(root) | |
| 212 | + | |
| 213 | + def move_player(self, target_path): | |
| 214 | + """Move player to a new location if valid""" | |
| 215 | + # Normalize path | |
| 216 | + if not target_path.startswith('/'): | |
| 217 | + # Relative path | |
| 218 | + if target_path == "..": | |
| 219 | + # Go up one directory | |
| 220 | + if self.player_location == "/": | |
| 221 | + return False, "Already at root directory" | |
| 222 | + self.player_location = "/".join(self.player_location.split("/")[:-1]) or "/" | |
| 223 | + else: | |
| 224 | + # Go to subdirectory | |
| 225 | + new_path = f"{self.player_location}/{target_path}" if self.player_location != "/" else f"/{target_path}" | |
| 226 | + if DirectoryNode.objects.filter(tree=self, path=new_path).exists(): | |
| 227 | + self.player_location = new_path | |
| 228 | + else: | |
| 229 | + return False, f"Directory not found: {target_path}" | |
| 230 | + else: | |
| 231 | + # Absolute path | |
| 232 | + if DirectoryNode.objects.filter(tree=self, path=target_path).exists(): | |
| 233 | + self.player_location = target_path | |
| 234 | + else: | |
| 235 | + return False, f"Directory not found: {target_path}" | |
| 236 | + | |
| 237 | + self.save() | |
| 238 | + return True, f"Moved to {self.player_location}" | |
| 239 | + | |
| 240 | + def check_win_condition(self): | |
| 241 | + """Check if player is in the same directory as the mole""" | |
| 242 | + return self.player_location == self.mole_location | |
| 243 | + | |
| 244 | + | |
| 245 | +class DirectoryNode(models.Model): | |
| 246 | + """A directory in the filesystem tree""" | |
| 247 | + tree = models.ForeignKey(FileSystemTree, on_delete=models.CASCADE, related_name='nodes') | |
| 248 | + parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children') | |
| 249 | + | |
| 250 | + name = models.CharField(max_length=100) | |
| 251 | + path = models.CharField(max_length=500, db_index=True) | |
| 252 | + is_fhs_standard = models.BooleanField(default=False) | |
| 253 | + description = models.TextField(blank=True) | |
| 254 | + | |
| 255 | + class Meta: | |
| 256 | + unique_together = ('tree', 'path') | |
| 257 | + ordering = ['path'] | |
| 258 | + | |
| 259 | + def __str__(self): | |
| 260 | + return f"{self.path} ({'FHS' if self.is_fhs_standard else 'Generated'})" | |
| 261 | + | |
| 262 | + @property | |
| 263 | + def depth(self): | |
| 264 | + """Calculate directory depth""" | |
| 265 | + return self.path.count('/') | |
| 266 | + | |
| 267 | + def get_contents(self): | |
| 268 | + """Get immediate children of this directory""" | |
| 269 | + return DirectoryNode.objects.filter(parent=self).order_by('name') | |
| 270 | + | |
| 271 | + | |
| 272 | +class GameSession(models.Model): | |
| 273 | + """Track game sessions and scores""" | |
| 274 | + tree = models.ForeignKey(FileSystemTree, on_delete=models.CASCADE, related_name='sessions') | |
| 275 | + player_name = models.CharField(max_length=100, default="Anonymous") | |
| 276 | + started_at = models.DateTimeField(auto_now_add=True) | |
| 277 | + completed_at = models.DateTimeField(null=True, blank=True) | |
| 278 | + | |
| 279 | + # Game metrics | |
| 280 | + commands_used = models.IntegerField(default=0) | |
| 281 | + directories_visited = models.IntegerField(default=0) | |
| 282 | + time_taken = models.DurationField(null=True, blank=True) | |
| 283 | + | |
| 284 | + # Command history | |
| 285 | + command_history = models.JSONField(default=list) | |
| 286 | + | |
| 287 | + def __str__(self): | |
| 288 | + return f"{self.player_name} - {self.tree.name}" | |
| 289 | + | |
| 290 | + def add_command(self, command): | |
| 291 | + """Add a command to the history""" | |
| 292 | + self.command_history.append({ | |
| 293 | + 'command': command, | |
| 294 | + 'timestamp': str(timezone.now()) | |
| 295 | + }) | |
| 296 | + self.commands_used += 1 | |
| 297 | + self.save() | |
backend/apps/trees/serializers.pyadded@@ -0,0 +1,47 @@ | ||
| 1 | +# apps/trees/serializers.py | |
| 2 | +from rest_framework import serializers | |
| 3 | +from .models import FileSystemTree, DirectoryNode, GameSession | |
| 4 | + | |
| 5 | + | |
| 6 | +class DirectoryNodeSerializer(serializers.ModelSerializer): | |
| 7 | + class Meta: | |
| 8 | + model = DirectoryNode | |
| 9 | + fields = ['id', 'name', 'path', 'is_fhs_standard', 'description', 'depth'] | |
| 10 | + read_only_fields = ['depth'] | |
| 11 | + | |
| 12 | + | |
| 13 | +class FileSystemTreeSerializer(serializers.ModelSerializer): | |
| 14 | + total_directories = serializers.SerializerMethodField() | |
| 15 | + | |
| 16 | + class Meta: | |
| 17 | + model = FileSystemTree | |
| 18 | + fields = [ | |
| 19 | + 'id', 'name', 'created_at', 'seed', | |
| 20 | + 'player_location', 'is_completed', 'completed_at', | |
| 21 | + 'tree_data', 'total_directories' | |
| 22 | + ] | |
| 23 | + read_only_fields = ['created_at', 'tree_data', 'total_directories'] | |
| 24 | + | |
| 25 | + def get_total_directories(self, obj): | |
| 26 | + return obj.nodes.count() | |
| 27 | + | |
| 28 | + | |
| 29 | +class GameSessionSerializer(serializers.ModelSerializer): | |
| 30 | + tree_name = serializers.CharField(source='tree.name', read_only=True) | |
| 31 | + | |
| 32 | + class Meta: | |
| 33 | + model = GameSession | |
| 34 | + fields = [ | |
| 35 | + 'id', 'tree', 'tree_name', 'player_name', | |
| 36 | + 'started_at', 'completed_at', 'commands_used', | |
| 37 | + 'directories_visited', 'time_taken', 'command_history' | |
| 38 | + ] | |
| 39 | + read_only_fields = [ | |
| 40 | + 'started_at', 'completed_at', 'time_taken', 'command_history' | |
| 41 | + ] | |
| 42 | + | |
| 43 | + | |
| 44 | +class GameCommandSerializer(serializers.Serializer): | |
| 45 | + """Serializer for game commands""" | |
| 46 | + command = serializers.CharField(max_length=200) | |
| 47 | + session_id = serializers.IntegerField(required=False) | |
backend/apps/trees/tests.pyadded@@ -0,0 +1,3 @@ | ||
| 1 | +from django.test import TestCase | |
| 2 | + | |
| 3 | +# Create your tests here. | |
backend/apps/trees/urls.pyadded@@ -0,0 +1,13 @@ | ||
| 1 | +# apps/trees/urls.py | |
| 2 | +from django.urls import path, include | |
| 3 | +from rest_framework.routers import DefaultRouter | |
| 4 | +from . import views | |
| 5 | + | |
| 6 | +router = DefaultRouter() | |
| 7 | +router.register(r'filesystem-trees', views.FileSystemTreeViewSet) | |
| 8 | +router.register(r'directories', views.DirectoryNodeViewSet) | |
| 9 | +router.register(r'game-sessions', views.GameSessionViewSet) | |
| 10 | + | |
| 11 | +urlpatterns = [ | |
| 12 | + path('', include(router.urls)), | |
| 13 | +] | |
backend/apps/trees/views.pyadded@@ -0,0 +1,231 @@ | ||
| 1 | +# apps/trees/views.py | |
| 2 | +from django.http import HttpResponse | |
| 3 | +from rest_framework import viewsets, status | |
| 4 | +from rest_framework.decorators import action | |
| 5 | +from rest_framework.response import Response | |
| 6 | +from django.utils import timezone | |
| 7 | +from datetime import timedelta | |
| 8 | +from .models import FileSystemTree, DirectoryNode, GameSession | |
| 9 | +from .serializers import ( | |
| 10 | + FileSystemTreeSerializer, DirectoryNodeSerializer, | |
| 11 | + GameSessionSerializer, GameCommandSerializer | |
| 12 | +) | |
| 13 | + | |
| 14 | + | |
| 15 | +class FileSystemTreeViewSet(viewsets.ModelViewSet): | |
| 16 | + """ViewSet for filesystem trees""" | |
| 17 | + queryset = FileSystemTree.objects.all() | |
| 18 | + serializer_class = FileSystemTreeSerializer | |
| 19 | + | |
| 20 | + @action(detail=False, methods=['post']) | |
| 21 | + def create_game(self, request): | |
| 22 | + """Create a new game with a generated filesystem tree""" | |
| 23 | + tree_name = request.data.get('name', 'FHS Game Tree') | |
| 24 | + max_depth = request.data.get('max_depth', 5) | |
| 25 | + dirs_per_level = request.data.get('dirs_per_level', 3) | |
| 26 | + | |
| 27 | + # Create and generate tree | |
| 28 | + tree = FileSystemTree.objects.create(name=tree_name) | |
| 29 | + tree.generate_tree(max_depth=max_depth, directories_per_level=dirs_per_level) | |
| 30 | + | |
| 31 | + # Create game session | |
| 32 | + player_name = request.data.get('player_name', 'Anonymous') | |
| 33 | + session = GameSession.objects.create( | |
| 34 | + tree=tree, | |
| 35 | + player_name=player_name | |
| 36 | + ) | |
| 37 | + | |
| 38 | + serializer = self.get_serializer(tree) | |
| 39 | + return Response({ | |
| 40 | + 'tree': serializer.data, | |
| 41 | + 'session_id': session.id, | |
| 42 | + 'mole_hint': f"The mole is hiding somewhere in the filesystem!" | |
| 43 | + }, status=status.HTTP_201_CREATED) | |
| 44 | + | |
| 45 | + @action(detail=True, methods=['get']) | |
| 46 | + def current_directory(self, request, pk=None): | |
| 47 | + """Get current directory contents and player location""" | |
| 48 | + tree = self.get_object() | |
| 49 | + | |
| 50 | + try: | |
| 51 | + current_dir = DirectoryNode.objects.get( | |
| 52 | + tree=tree, | |
| 53 | + path=tree.player_location | |
| 54 | + ) | |
| 55 | + contents = current_dir.get_contents() | |
| 56 | + | |
| 57 | + return Response({ | |
| 58 | + 'path': tree.player_location, | |
| 59 | + 'contents': DirectoryNodeSerializer(contents, many=True).data, | |
| 60 | + 'parent': current_dir.parent.path if current_dir.parent else None | |
| 61 | + }) | |
| 62 | + except DirectoryNode.DoesNotExist: | |
| 63 | + return Response( | |
| 64 | + {'error': 'Current directory not found'}, | |
| 65 | + status=status.HTTP_404_NOT_FOUND | |
| 66 | + ) | |
| 67 | + | |
| 68 | + @action(detail=True, methods=['post']) | |
| 69 | + def execute_command(self, request, pk=None): | |
| 70 | + """Execute a shell command in the game""" | |
| 71 | + tree = self.get_object() | |
| 72 | + command = request.data.get('command', '').strip() | |
| 73 | + session_id = request.data.get('session_id') | |
| 74 | + | |
| 75 | + if not command: | |
| 76 | + return Response( | |
| 77 | + {'error': 'No command provided'}, | |
| 78 | + status=status.HTTP_400_BAD_REQUEST | |
| 79 | + ) | |
| 80 | + | |
| 81 | + # Get session if provided | |
| 82 | + session = None | |
| 83 | + if session_id: | |
| 84 | + try: | |
| 85 | + session = GameSession.objects.get(id=session_id, tree=tree) | |
| 86 | + session.add_command(command) | |
| 87 | + except GameSession.DoesNotExist: | |
| 88 | + pass | |
| 89 | + | |
| 90 | + # Parse and execute command | |
| 91 | + parts = command.split() | |
| 92 | + cmd = parts[0] if parts else "" | |
| 93 | + | |
| 94 | + response_data = { | |
| 95 | + 'command': command, | |
| 96 | + 'success': False, | |
| 97 | + 'output': '', | |
| 98 | + 'current_path': tree.player_location | |
| 99 | + } | |
| 100 | + | |
| 101 | + if cmd == 'cd': | |
| 102 | + if len(parts) < 2: | |
| 103 | + response_data['output'] = "cd: missing operand" | |
| 104 | + else: | |
| 105 | + target = parts[1] | |
| 106 | + success, message = tree.move_player(target) | |
| 107 | + response_data['success'] = success | |
| 108 | + response_data['output'] = message | |
| 109 | + response_data['current_path'] = tree.player_location | |
| 110 | + | |
| 111 | + if success and session: | |
| 112 | + session.directories_visited += 1 | |
| 113 | + session.save() | |
| 114 | + | |
| 115 | + elif cmd == 'ls': | |
| 116 | + try: | |
| 117 | + current_dir = DirectoryNode.objects.get( | |
| 118 | + tree=tree, | |
| 119 | + path=tree.player_location | |
| 120 | + ) | |
| 121 | + contents = current_dir.get_contents() | |
| 122 | + if contents: | |
| 123 | + response_data['output'] = '\n'.join([d.name for d in contents]) | |
| 124 | + else: | |
| 125 | + response_data['output'] = '' | |
| 126 | + response_data['success'] = True | |
| 127 | + except DirectoryNode.DoesNotExist: | |
| 128 | + response_data['output'] = "ls: cannot access directory" | |
| 129 | + | |
| 130 | + elif cmd == 'pwd': | |
| 131 | + response_data['output'] = tree.player_location | |
| 132 | + response_data['success'] = True | |
| 133 | + | |
| 134 | + elif cmd == 'killall' and len(parts) > 1 and parts[1] == 'moles': | |
| 135 | + if tree.check_win_condition(): | |
| 136 | + tree.is_completed = True | |
| 137 | + tree.completed_at = timezone.now() | |
| 138 | + tree.save() | |
| 139 | + | |
| 140 | + if session: | |
| 141 | + session.completed_at = timezone.now() | |
| 142 | + session.time_taken = session.completed_at - session.started_at | |
| 143 | + session.save() | |
| 144 | + | |
| 145 | + response_data['output'] = "🎉 Congratulations! You found and eliminated the mole!" | |
| 146 | + response_data['success'] = True | |
| 147 | + response_data['game_won'] = True | |
| 148 | + else: | |
| 149 | + response_data['output'] = "No moles found in this directory." | |
| 150 | + response_data['success'] = True | |
| 151 | + | |
| 152 | + elif cmd == 'help': | |
| 153 | + response_data['output'] = """Available commands: | |
| 154 | +cd <directory> - Change directory | |
| 155 | +ls - List directory contents | |
| 156 | +pwd - Print working directory | |
| 157 | +killall moles - Eliminate moles (when in the same directory) | |
| 158 | +help - Show this help message""" | |
| 159 | + response_data['success'] = True | |
| 160 | + | |
| 161 | + else: | |
| 162 | + response_data['output'] = f"bash: {cmd}: command not found" | |
| 163 | + | |
| 164 | + return Response(response_data) | |
| 165 | + | |
| 166 | + @action(detail=True, methods=['get']) | |
| 167 | + def hint(self, request, pk=None): | |
| 168 | + """Get a hint about the mole's location""" | |
| 169 | + tree = self.get_object() | |
| 170 | + | |
| 171 | + if not tree.mole_location: | |
| 172 | + return Response({'hint': 'No mole in this tree!'}) | |
| 173 | + | |
| 174 | + mole_depth = tree.mole_location.count('/') | |
| 175 | + player_depth = tree.player_location.count('/') | |
| 176 | + | |
| 177 | + hints = [] | |
| 178 | + if mole_depth > player_depth: | |
| 179 | + hints.append("The mole is deeper in the filesystem than you are.") | |
| 180 | + elif mole_depth < player_depth: | |
| 181 | + hints.append("The mole is in a shallower directory than you are.") | |
| 182 | + else: | |
| 183 | + hints.append("You're at the same depth as the mole!") | |
| 184 | + | |
| 185 | + # Give a path hint | |
| 186 | + mole_parts = tree.mole_location.split('/') | |
| 187 | + player_parts = tree.player_location.split('/') | |
| 188 | + | |
| 189 | + # Find common path | |
| 190 | + common_parts = [] | |
| 191 | + for i, (m, p) in enumerate(zip(mole_parts, player_parts)): | |
| 192 | + if m == p: | |
| 193 | + common_parts.append(m) | |
| 194 | + else: | |
| 195 | + break | |
| 196 | + | |
| 197 | + if len(common_parts) == len(mole_parts): | |
| 198 | + hints.append("You're in the mole's directory! Use 'killall moles'!") | |
| 199 | + elif len(common_parts) > 1: | |
| 200 | + hints.append(f"You share a common path with the mole: {'/'.join(common_parts) or '/'}") | |
| 201 | + | |
| 202 | + return Response({'hints': hints}) | |
| 203 | + | |
| 204 | + | |
| 205 | +class DirectoryNodeViewSet(viewsets.ReadOnlyModelViewSet): | |
| 206 | + """ViewSet for browsing directory nodes""" | |
| 207 | + queryset = DirectoryNode.objects.all() | |
| 208 | + serializer_class = DirectoryNodeSerializer | |
| 209 | + | |
| 210 | + def get_queryset(self): | |
| 211 | + queryset = super().get_queryset() | |
| 212 | + tree_id = self.request.query_params.get('tree', None) | |
| 213 | + if tree_id: | |
| 214 | + queryset = queryset.filter(tree_id=tree_id) | |
| 215 | + return queryset | |
| 216 | + | |
| 217 | + | |
| 218 | +class GameSessionViewSet(viewsets.ModelViewSet): | |
| 219 | + """ViewSet for game sessions""" | |
| 220 | + queryset = GameSession.objects.all() | |
| 221 | + serializer_class = GameSessionSerializer | |
| 222 | + | |
| 223 | + @action(detail=False, methods=['get']) | |
| 224 | + def leaderboard(self, request): | |
| 225 | + """Get the leaderboard of fastest completions""" | |
| 226 | + completed_sessions = GameSession.objects.filter( | |
| 227 | + completed_at__isnull=False | |
| 228 | + ).order_by('time_taken', 'commands_used')[:20] | |
| 229 | + | |
| 230 | + serializer = self.get_serializer(completed_sessions, many=True) | |
| 231 | + return Response(serializer.data) | |
backend/bashamole/__init__.pyaddedbackend/bashamole/asgi.pyadded@@ -0,0 +1,16 @@ | ||
| 1 | +""" | |
| 2 | +ASGI config for bashamole project. | |
| 3 | + | |
| 4 | +It exposes the ASGI callable as a module-level variable named ``application``. | |
| 5 | + | |
| 6 | +For more information on this file, see | |
| 7 | +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ | |
| 8 | +""" | |
| 9 | + | |
| 10 | +import os | |
| 11 | + | |
| 12 | +from django.core.asgi import get_asgi_application | |
| 13 | + | |
| 14 | +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bashamole.settings") | |
| 15 | + | |
| 16 | +application = get_asgi_application() | |
backend/bashamole/settings.pyadded@@ -0,0 +1,138 @@ | ||
| 1 | +""" | |
| 2 | +Django settings for bashamole project. | |
| 3 | + | |
| 4 | +Generated by 'django-admin startproject' using Django 5.2.3. | |
| 5 | + | |
| 6 | +For more information on this file, see | |
| 7 | +https://docs.djangoproject.com/en/5.2/topics/settings/ | |
| 8 | + | |
| 9 | +For the full list of settings and their values, see | |
| 10 | +https://docs.djangoproject.com/en/5.2/ref/settings/ | |
| 11 | +""" | |
| 12 | + | |
| 13 | +from pathlib import Path | |
| 14 | + | |
| 15 | +# Build paths inside the project like this: BASE_DIR / 'subdir'. | |
| 16 | +BASE_DIR = Path(__file__).resolve().parent.parent | |
| 17 | + | |
| 18 | + | |
| 19 | +# Quick-start development settings - unsuitable for production | |
| 20 | +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ | |
| 21 | + | |
| 22 | +# SECURITY WARNING: keep the secret key used in production secret! | |
| 23 | +SECRET_KEY = "django-insecure-xykf5$__a(xn*avfxs2njr#==cn7*+t8kg+4_lbi$(#+@5_j8g" | |
| 24 | + | |
| 25 | +# SECURITY WARNING: don't run with debug turned on in production! | |
| 26 | +DEBUG = True | |
| 27 | + | |
| 28 | +ALLOWED_HOSTS = [] | |
| 29 | + | |
| 30 | + | |
| 31 | +# Application definition | |
| 32 | + | |
| 33 | +INSTALLED_APPS = [ | |
| 34 | + "django.contrib.admin", | |
| 35 | + "django.contrib.auth", | |
| 36 | + "django.contrib.contenttypes", | |
| 37 | + "django.contrib.sessions", | |
| 38 | + "django.contrib.messages", | |
| 39 | + "django.contrib.staticfiles", | |
| 40 | + "rest_framework", | |
| 41 | + "corsheaders", | |
| 42 | + "apps.trees", | |
| 43 | +] | |
| 44 | + | |
| 45 | +MIDDLEWARE = [ | |
| 46 | + "django.middleware.security.SecurityMiddleware", | |
| 47 | + "django.contrib.sessions.middleware.SessionMiddleware", | |
| 48 | + "django.middleware.common.CommonMiddleware", | |
| 49 | + "django.middleware.csrf.CsrfViewMiddleware", | |
| 50 | + "django.contrib.auth.middleware.AuthenticationMiddleware", | |
| 51 | + "django.contrib.messages.middleware.MessageMiddleware", | |
| 52 | + "django.middleware.clickjacking.XFrameOptionsMiddleware", | |
| 53 | + 'corsheaders.middleware.CorsMiddleware', | |
| 54 | + 'django.middleware.common.CommonMiddleware', | |
| 55 | +] | |
| 56 | + | |
| 57 | +ROOT_URLCONF = "bashamole.urls" | |
| 58 | + | |
| 59 | +TEMPLATES = [ | |
| 60 | + { | |
| 61 | + "BACKEND": "django.template.backends.django.DjangoTemplates", | |
| 62 | + "DIRS": [], | |
| 63 | + "APP_DIRS": True, | |
| 64 | + "OPTIONS": { | |
| 65 | + "context_processors": [ | |
| 66 | + "django.template.context_processors.request", | |
| 67 | + "django.contrib.auth.context_processors.auth", | |
| 68 | + "django.contrib.messages.context_processors.messages", | |
| 69 | + ], | |
| 70 | + }, | |
| 71 | + }, | |
| 72 | +] | |
| 73 | + | |
| 74 | +WSGI_APPLICATION = "bashamole.wsgi.application" | |
| 75 | + | |
| 76 | + | |
| 77 | +# Database | |
| 78 | +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases | |
| 79 | + | |
| 80 | +DATABASES = { | |
| 81 | + "default": { | |
| 82 | + "ENGINE": "django.db.backends.sqlite3", | |
| 83 | + "NAME": BASE_DIR / "db.sqlite3", | |
| 84 | + } | |
| 85 | +} | |
| 86 | + | |
| 87 | + | |
| 88 | +# Password validation | |
| 89 | +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators | |
| 90 | + | |
| 91 | +AUTH_PASSWORD_VALIDATORS = [ | |
| 92 | + { | |
| 93 | + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", | |
| 94 | + }, | |
| 95 | + { | |
| 96 | + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", | |
| 97 | + }, | |
| 98 | + { | |
| 99 | + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", | |
| 100 | + }, | |
| 101 | + { | |
| 102 | + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", | |
| 103 | + }, | |
| 104 | +] | |
| 105 | + | |
| 106 | + | |
| 107 | +# Internationalization | |
| 108 | +# https://docs.djangoproject.com/en/5.2/topics/i18n/ | |
| 109 | + | |
| 110 | +LANGUAGE_CODE = "en-us" | |
| 111 | + | |
| 112 | +TIME_ZONE = "UTC" | |
| 113 | + | |
| 114 | +USE_I18N = True | |
| 115 | + | |
| 116 | +USE_TZ = True | |
| 117 | + | |
| 118 | + | |
| 119 | +# Static files (CSS, JavaScript, Images) | |
| 120 | +# https://docs.djangoproject.com/en/5.2/howto/static-files/ | |
| 121 | + | |
| 122 | +STATIC_URL = "static/" | |
| 123 | + | |
| 124 | +# Default primary key field type | |
| 125 | +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field | |
| 126 | + | |
| 127 | +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" | |
| 128 | + | |
| 129 | +CORS_ALLOWED_ORIGINS = [ | |
| 130 | + "http://localhost:3000", | |
| 131 | + "http://localhost:5173", | |
| 132 | +] | |
| 133 | + | |
| 134 | +# API settings | |
| 135 | +REST_FRAMEWORK = { | |
| 136 | + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', | |
| 137 | + 'PAGE_SIZE': 100 | |
| 138 | +} | |
backend/bashamole/urls.pyadded@@ -0,0 +1,25 @@ | ||
| 1 | +""" | |
| 2 | +URL configuration for bashamole project. | |
| 3 | + | |
| 4 | +The `urlpatterns` list routes URLs to views. For more information please see: | |
| 5 | + https://docs.djangoproject.com/en/5.2/topics/http/urls/ | |
| 6 | +Examples: | |
| 7 | +Function views | |
| 8 | + 1. Add an import: from my_app import views | |
| 9 | + 2. Add a URL to urlpatterns: path('', views.home, name='home') | |
| 10 | +Class-based views | |
| 11 | + 1. Add an import: from other_app.views import Home | |
| 12 | + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') | |
| 13 | +Including another URLconf | |
| 14 | + 1. Import the include() function: from django.urls import include, path | |
| 15 | + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) | |
| 16 | +""" | |
| 17 | + | |
| 18 | +from django.contrib import admin | |
| 19 | +from django.urls import path, include | |
| 20 | + | |
| 21 | +urlpatterns = [ | |
| 22 | + path('admin/', admin.site.urls), | |
| 23 | + path('api/trees/', include('apps.trees.urls')), | |
| 24 | + path('api-auth/', include('rest_framework.urls')), | |
| 25 | +] | |
backend/bashamole/wsgi.pyadded@@ -0,0 +1,16 @@ | ||
| 1 | +""" | |
| 2 | +WSGI config for bashamole project. | |
| 3 | + | |
| 4 | +It exposes the WSGI callable as a module-level variable named ``application``. | |
| 5 | + | |
| 6 | +For more information on this file, see | |
| 7 | +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ | |
| 8 | +""" | |
| 9 | + | |
| 10 | +import os | |
| 11 | + | |
| 12 | +from django.core.wsgi import get_wsgi_application | |
| 13 | + | |
| 14 | +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bashamole.settings") | |
| 15 | + | |
| 16 | +application = get_wsgi_application() | |
backend/manage.pyadded@@ -0,0 +1,22 @@ | ||
| 1 | +#!/usr/bin/env python | |
| 2 | +"""Django's command-line utility for administrative tasks.""" | |
| 3 | +import os | |
| 4 | +import sys | |
| 5 | + | |
| 6 | + | |
| 7 | +def main(): | |
| 8 | + """Run administrative tasks.""" | |
| 9 | + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bashamole.settings") | |
| 10 | + try: | |
| 11 | + from django.core.management import execute_from_command_line | |
| 12 | + except ImportError as exc: | |
| 13 | + raise ImportError( | |
| 14 | + "Couldn't import Django. Are you sure it's installed and " | |
| 15 | + "available on your PYTHONPATH environment variable? Did you " | |
| 16 | + "forget to activate a virtual environment?" | |
| 17 | + ) from exc | |
| 18 | + execute_from_command_line(sys.argv) | |
| 19 | + | |
| 20 | + | |
| 21 | +if __name__ == "__main__": | |
| 22 | + main() | |
backend/requirements.txtadded@@ -0,0 +1,5 @@ | ||
| 1 | +asgiref==3.8.1 | |
| 2 | +Django==5.2.3 | |
| 3 | +django-cors-headers==4.7.0 | |
| 4 | +djangorestframework==3.16.0 | |
| 5 | +sqlparse==0.5.3 | |
package.jsonadded@@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "name": "bashamole", | |
| 3 | + "version": "1.0.0", | |
| 4 | + "private": true, | |
| 5 | + "scripts": { | |
| 6 | + "dev:backend": "cd backend && python manage.py runserver", | |
| 7 | + "dev:frontend": "cd frontend && npm run dev", | |
| 8 | + "install:backend": "cd backend && pip install -r requirements.txt", | |
| 9 | + "install:frontend": "cd frontend && npm install" | |
| 10 | + }, | |
| 11 | + "dependencies": { | |
| 12 | + "@types/d3": "^7.4.3", | |
| 13 | + "axios": "^1.10.0", | |
| 14 | + "d3": "^7.9.0" | |
| 15 | + }, | |
| 16 | + "devDependencies": { | |
| 17 | + "autoprefixer": "^10.4.21", | |
| 18 | + "postcss": "^8.5.5", | |
| 19 | + "tailwindcss": "^4.1.10" | |
| 20 | + } | |
| 21 | +} | |