diff --git a/python_anywhere_website/bible/admin.py b/python_anywhere_website/bible/admin.py index 7346c2b..0054c39 100644 --- a/python_anywhere_website/bible/admin.py +++ b/python_anywhere_website/bible/admin.py @@ -1,5 +1,54 @@ -from django.contrib import admin +from django.contrib import admin, messages +from django.core.management import call_command from .models import BibleBook, BibleVerse +import threading +import logging + +logger = logging.getLogger(__name__) + + +def _run_import_kjv(caller_name=None): + """ + Helper that runs the management command. Intended to run in a background thread. + """ + try: + logger.info("Admin-initiated KJV import started (user=%s)", caller_name) + # Always call with clear=True to ensure a clean import (per requirements) + call_command('import_kjv', clear=True) + logger.info("Admin-initiated KJV import completed (user=%s)", caller_name) + except Exception: + logger.exception("Admin-initiated KJV import failed (user=%s)", caller_name) + + +def import_kjv_action(modeladmin, request, queryset): + """ + Admin action to start the KJV import in a background thread. + - Visible in the actions dropdown on the BibleBook changelist. + - Uses standard admin permissions (requires change permission). + - Starts the import in a daemon thread and returns immediately. + """ + # Limit to staff users as a safety check (admin actions normally require appropriate perms) + if not request.user.is_staff: + modeladmin.message_user(request, "Only staff users may run the KJV import.", level=messages.ERROR) + return + + caller = getattr(request.user, 'username', str(request.user)) + try: + thread = threading.Thread(target=_run_import_kjv, args=(caller,), daemon=True) + thread.start() + modeladmin.message_user( + request, + "KJV import has been started in the background. Check the server logs for progress and errors.", + level=messages.INFO, + ) + except Exception as exc: + logger.exception("Failed to start KJV import thread (user=%s)", caller) + modeladmin.message_user(request, f"Failed to start import: {exc}", level=messages.ERROR) + + +# Action metadata for admin UI +import_kjv_action.short_description = "Import KJV Bible" +import_kjv_action.allowed_permissions = ('change',) @admin.register(BibleBook) @@ -10,6 +59,9 @@ class BibleBookAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('name',)} ordering = ['order'] + # Register the import action on the changelist actions dropdown + actions = [import_kjv_action] + @admin.register(BibleVerse) class BibleVerseAdmin(admin.ModelAdmin): diff --git a/python_anywhere_website/bible/tests.py b/python_anywhere_website/bible/tests.py index 5d1bbf7..80a1390 100644 --- a/python_anywhere_website/bible/tests.py +++ b/python_anywhere_website/bible/tests.py @@ -1,7 +1,10 @@ -from django.test import TestCase, Client +from django.test import TestCase, Client, RequestFactory from django.urls import reverse +from django.contrib.auth.models import User +from django.contrib.messages.storage.fallback import FallbackStorage from bible.models import BibleBook, BibleVerse from bible.gutenberg_parser import parse_gutenberg_kjv, BOOK_INFO +from bible.admin import import_kjv_action, BibleBookAdmin import tempfile import os @@ -351,3 +354,80 @@ def test_api_rate_limit_headers(self): self.assertIn('X-RateLimit-Remaining', response) self.assertEqual(response['X-RateLimit-Limit'], '100') + +class AdminActionTest(TestCase): + """Tests for admin actions.""" + + def setUp(self): + self.user_staff = User.objects.create_user( + username='staffuser', + password='testpass', + is_staff=True + ) + self.user_nonstaff = User.objects.create_user( + username='normaluser', + password='testpass', + is_staff=False + ) + self.book = BibleBook.objects.create( + name='John', + slug='john', + order=43, + testament='NT', + chapters=21 + ) + self.admin = BibleBookAdmin(BibleBook, None) + + def test_import_kjv_action_requires_staff(self): + """Test that non-staff users cannot run the import action.""" + factory = RequestFactory() + request = factory.post('/admin/bible/biblebook/') + request.user = self.user_nonstaff + + # Add message support to request + setattr(request, 'session', {}) + messages = FallbackStorage(request) + setattr(request, '_messages', messages) + + # Run the action + queryset = BibleBook.objects.all() + import_kjv_action(self.admin, request, queryset) + + # Check that an error message was sent + message_list = list(messages) + self.assertEqual(len(message_list), 1) + self.assertIn('Only staff users', str(message_list[0])) + + def test_import_kjv_action_starts_thread_for_staff(self): + """Test that staff users can start the import action.""" + factory = RequestFactory() + request = factory.post('/admin/bible/biblebook/') + request.user = self.user_staff + + # Add message support to request + setattr(request, 'session', {}) + messages = FallbackStorage(request) + setattr(request, '_messages', messages) + + # Run the action + queryset = BibleBook.objects.all() + import_kjv_action(self.admin, request, queryset) + + # Check that a success message was sent + message_list = list(messages) + self.assertEqual(len(message_list), 1) + self.assertIn('started in the background', str(message_list[0])) + + def test_import_kjv_action_metadata(self): + """Test that the action has proper metadata.""" + self.assertEqual(import_kjv_action.short_description, "Import KJV Bible") + self.assertEqual(import_kjv_action.allowed_permissions, ('change',)) + + def test_admin_has_action_registered(self): + """Test that BibleBookAdmin has the import action registered.""" + # Get the admin class + admin = BibleBookAdmin(BibleBook, None) + + # Check that actions includes our import action + self.assertIn(import_kjv_action, admin.actions) +