From 7125d65a393e97449df924a092a7652066d903e5 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sun, 4 Jan 2026 00:13:59 +0330 Subject: [PATCH] admin panel fieldsets fixed. --- .../management/commands/test_guest_token.py | 37 ++++ apps/hadis/admin/transmitter.py | 4 +- config/debug_authentication.py | 48 +++++ config/settings/base.py | 4 +- .../{fieldset.html => fieldset.html.backup} | 0 utils/admin.py | 200 +++++++++++++++++- utils/permissions.py | 14 ++ 7 files changed, 296 insertions(+), 11 deletions(-) create mode 100644 apps/account/management/commands/test_guest_token.py create mode 100644 config/debug_authentication.py rename templates/admin/includes/{fieldset.html => fieldset.html.backup} (100%) create mode 100644 utils/permissions.py diff --git a/apps/account/management/commands/test_guest_token.py b/apps/account/management/commands/test_guest_token.py new file mode 100644 index 0000000..fb1c228 --- /dev/null +++ b/apps/account/management/commands/test_guest_token.py @@ -0,0 +1,37 @@ +from django.core.management.base import BaseCommand +from rest_framework.test import APIClient +from apps.account.models import User # Your user model +import json + +class Command(BaseCommand): + def handle(self, *args, **options): + client = APIClient() + + # Step 1: Create guest token + print("\nšŸ“ Step 1: Creating guest token...") + response = client.post('/api/account/web/guest/', + data=json.dumps({"timezone": "UTC", "user_agent": "test"}), + content_type='application/json' + ) + print(f"Status: {response.status_code}") + if response.status_code == 200: + print(f"Response: {response.json()}") + token = response.json()['token'] + else: + print(f"Error Response: {response.content.decode('utf-8')}") + print("āŒ Failed to create token!") + return + print(f"āœ… Token created: {token[:20]}...") + + # Step 2: Test authentication with token + print("\nšŸ” Step 2: Testing token authentication...") + client.credentials(HTTP_AUTHORIZATION=f'Token {token}') + + response = client.get('/api/library/books/') + print(f"Status: {response.status_code}") + if response.status_code == 200: + print(f"Response: {response.json()}") + print("āœ… Token authentication works!") + else: + print(f"Error Response: {response.content.decode('utf-8')}") + print("āŒ Token authentication failed!") \ No newline at end of file diff --git a/apps/hadis/admin/transmitter.py b/apps/hadis/admin/transmitter.py index 05bcee4..89e4bde 100644 --- a/apps/hadis/admin/transmitter.py +++ b/apps/hadis/admin/transmitter.py @@ -48,11 +48,11 @@ class TransmittersAdmin(ModelAdmin): }), (_('Additional Information'), { 'fields': ('description',), - 'classes': ('collapse',) + # 'classes': ('collapse',) }), (_('Timestamps'), { 'fields': ('created_at', 'updated_at'), - 'classes': ('collapse',) + # 'classes': ('collapse',) }), ) @display(description=_('Full Name'), ordering='full_name') diff --git a/config/debug_authentication.py b/config/debug_authentication.py new file mode 100644 index 0000000..14e3793 --- /dev/null +++ b/config/debug_authentication.py @@ -0,0 +1,48 @@ +from rest_framework.authentication import TokenAuthentication +from rest_framework.exceptions import AuthenticationFailed +import logging + +logger = logging.getLogger(__name__) + +class DebugTokenAuthentication(TokenAuthentication): + """ + Extended TokenAuthentication with detailed logging for debugging + """ + def authenticate(self, request): + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + logger.info(f"šŸ” AUTH DEBUG - Header: {auth_header}") + + # Check if header exists + if not auth_header: + logger.warning("šŸ”“ AUTH DEBUG - No Authorization header found") + return None + + # Extract token + parts = auth_header.split() + if len(parts) != 2 or parts[0].lower() != 'token': + logger.warning(f"šŸ”“ AUTH DEBUG - Invalid header format: {parts}") + return None + + token_key = parts[1] + logger.info(f"šŸ” AUTH DEBUG - Token key extracted: {token_key[:10]}...") + + try: + # Try to get token from database + from rest_framework.authtoken.models import Token + token = Token.objects.select_related('user').get(key=token_key) + logger.info(f"āœ… AUTH DEBUG - Token found in DB") + logger.info(f"āœ… AUTH DEBUG - User: {token.user}") + logger.info(f"āœ… AUTH DEBUG - User ID: {token.user.id}") + logger.info(f"āœ… AUTH DEBUG - User is_active: {token.user.is_active}") + logger.info(f"āœ… AUTH DEBUG - User is_authenticated: {token.user.is_authenticated}") + + if not token.user.is_active: + logger.error("šŸ”“ AUTH DEBUG - User is not active") + raise AuthenticationFailed('User inactive or deleted.') + + logger.info("āœ… AUTH DEBUG - Authentication SUCCESSFUL") + return (token.user, token) + + except Exception as e: + logger.error(f"šŸ”“ AUTH DEBUG - Token lookup failed: {str(e)}") + raise AuthenticationFailed('Invalid token.') \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 883c813..c2cc765 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -55,7 +55,7 @@ LOCAL_APPS = [ 'apps.bookmark.apps.BookmarkConfig', 'apps.article.apps.ArticleConfig', 'apps.dobodbi_calendar.apps.DobodbiCalendarConfig', - 'apps.blog.apps.BlogConfig', + 'apps.blog.apps.BlogConfig', 'dynamic_preferences', ] @@ -422,7 +422,7 @@ UNFOLD = { { "title": _("Guest Users"), "icon": "sports_motorsports", - "link": lambda request: f"{reverse_lazy('admin:account_user_changelist')}?email__isnull=true", + "link": lambda request: f"{admin_url_generator(request, 'account_user_changelist')}?email__isnull=true", }, ], }, diff --git a/templates/admin/includes/fieldset.html b/templates/admin/includes/fieldset.html.backup similarity index 100% rename from templates/admin/includes/fieldset.html rename to templates/admin/includes/fieldset.html.backup diff --git a/utils/admin.py b/utils/admin.py index 4e54939..938aa95 100644 --- a/utils/admin.py +++ b/utils/admin.py @@ -5,11 +5,13 @@ from functools import lru_cache from django import forms from django.conf import settings from django.contrib.humanize.templatetags.humanize import intcomma -from django.urls import reverse +from django.urls import reverse, path from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django.views.generic import RedirectView from django.utils.translation import get_language +from django.http import JsonResponse +from django.views.decorators.http import require_POST # Unfold Imports from unfold.sites import UnfoldAdminSite @@ -35,17 +37,20 @@ def admin_url_generator(request, url_name): Dynamically generates admin URLs based on the current active panel. Usage in settings.py: lambda request: admin_url_generator(request, "app_model_changelist") """ - # 1. Determine the current namespace using the robust check + # Ensure admin sites are created and URLs are registered + _ = project_admin_site.urls # Access URLs to ensure site is created + _ = dovoodi_admin_site.urls # Access URLs to ensure site is created + + # 1. Determine the current namespace if is_dovoodi_panel(request): namespace = 'dovoodi_admin' else: - # Default to the main admin namespace = 'imam_javad_admin' # 2. Construct the view name full_view_name = f"{namespace}:{url_name}" - # 3. Resolve the URL + # 3. Try Django URL reversal try: return reverse(full_view_name) except Exception: @@ -58,6 +63,14 @@ def dashboard_callback(request, context): def variables(request): return {"plausible_domain": getattr(settings, 'PLAUSIBLE_DOMAIN', '')} +# Toggle sidebar view for Unfold compatibility +@require_POST +def toggle_sidebar(request): + """Toggle sidebar state for Unfold admin interface""" + # This is a simple view that just returns success + # The actual sidebar state is handled client-side + return JsonResponse({'status': 'success'}) + # --------------------------------------------------------- # 2. Custom Login Form # --------------------------------------------------------- @@ -95,6 +108,18 @@ class FormulaAdminSite(UnfoldAdminSite): super().__init__(*args, **kwargs) # Set login form after initialization to avoid circular import self.login_form = LoginForm.get_form() + + def get_form(self, request, obj=None, **kwargs): + """Override to ensure form is properly initialized""" + form = super().get_form(request, obj, **kwargs) + return form + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'), + ] + return custom_urls + urls def _get_colors(self, key, *args): """Override colors for Imam Javad admin panel with green theme""" @@ -167,6 +192,18 @@ class DovoodiAdminSite(UnfoldAdminSite): super().__init__(*args, **kwargs) # Set login form after initialization to avoid circular import self.login_form = LoginForm.get_form() + + def get_form(self, request, obj=None, **kwargs): + """Override to ensure form is properly initialized""" + form = super().get_form(request, obj, **kwargs) + return form + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'), + ] + return custom_urls + urls def _get_colors(self, key, *args): """Override colors for Dovoodi admin panel with blue/teal theme matching frontend""" @@ -240,6 +277,7 @@ class AdminSitePlaceholder(UnfoldAdminSite): self._site_class = site_class self._name = name self._real_instance = None + self._registry = {} # Store registrations until real instance is created # 2. THE FIX: Copy visual attributes immediately so Templates see them! self.site_header = getattr(site_class, 'site_header', 'Django Admin') @@ -258,12 +296,28 @@ class AdminSitePlaceholder(UnfoldAdminSite): for attr in ['site_header', 'site_title', 'index_title', 'site_subheader']: if hasattr(self._real_instance, attr): setattr(self, attr, getattr(self._real_instance, attr)) + + # Copy any existing registrations from the placeholder to the real instance + if hasattr(self, '_registry'): + for model, admin_class in self._registry.items(): + self._real_instance.register(model, admin_class) + + # Replace the global reference with the real instance + import sys + current_module = sys.modules[__name__] + if hasattr(current_module, self._name): + setattr(current_module, self._name, self._real_instance) + return self._real_instance def __getattr__(self, name): # Delegate all attribute access to the real instance for proper CSS and template loading return getattr(self._get_real_instance(), name) + def get_form(self, request, obj=None, **kwargs): + """Delegate get_form to the real admin site instance""" + return self._get_real_instance().get_form(request, obj, **kwargs) + def __call__(self, *args, **kwargs): return self._get_real_instance()(*args, **kwargs) @@ -277,9 +331,141 @@ class AdminSitePlaceholder(UnfoldAdminSite): def each_context(self, request): return self._get_real_instance().each_context(request) -# Create placeholder instances that will be replaced with real instances when Django is ready -project_admin_site = AdminSitePlaceholder(FormulaAdminSite, 'imam_javad_admin') -dovoodi_admin_site = AdminSitePlaceholder(DovoodiAdminSite, 'dovoodi_admin') + def register(self, model_or_iterable, admin_class=None, **options): + """Store registrations in placeholder until real instance is created""" + if isinstance(model_or_iterable, (list, tuple)): + for model in model_or_iterable: + self.register(model, admin_class, **options) + else: + model = model_or_iterable + if model in self._registry: + # If already registered, update the admin class + self._registry[model] = admin_class + else: + self._registry[model] = admin_class + + # Also register with the real instance if it exists + if self._real_instance is not None: + self._real_instance.register(model, admin_class, **options) + +# Create lazy-loading admin site instances that properly inherit from AdminSite +class LazyAdminSite(UnfoldAdminSite): + def __init__(self, site_class, name): + # Don't call super().__init__() to avoid creating the real instance yet + self._site_class = site_class + self._name = name + self._instance = None + # Set basic attributes that Django expects for isinstance checks + self.name = name + + def _force_init(self): + """Force initialization immediately""" + if self._instance is None: + self._instance = self._site_class(name=self._name) + # Copy all attributes to this instance + for attr in dir(self._instance): + if not attr.startswith('_') and attr not in ('register', 'unregister', 'is_registered'): + try: + setattr(self, attr, getattr(self._instance, attr)) + except (AttributeError, TypeError): + pass + def _ensure_instance(self): + """Ensure the real instance exists""" + if self._instance is None: + self._instance = self._site_class(name=self._name) + # Copy essential attributes to this lazy wrapper for compatibility + essential_attrs = ['site_header', 'site_title', 'index_title', 'site_url', 'login_template'] + for attr in essential_attrs: + if hasattr(self._instance, attr): + setattr(self, attr, getattr(self._instance, attr)) + + def _get_instance(self): + self._ensure_instance() + return self._instance + + def __getattr__(self, name): + self._ensure_instance() + return getattr(self._instance, name) + + def __call__(self, *args, **kwargs): + return self._get_instance()(*args, **kwargs) + + @property + def urls(self): + """Ensure URLs are accessed to create the instance""" + return self._get_instance().urls + + def get_urls(self): + """Delegate get_urls to ensure proper URL registration""" + return self._get_instance().get_urls() + + def register(self, model_or_iterable, admin_class=None, **options): + """Register models with the real admin site instance""" + self._ensure_instance() + return self._instance.register(model_or_iterable, admin_class, **options) + +# Create lazy admin site instances +project_admin_site = LazyAdminSite(FormulaAdminSite, 'imam_javad_admin') +dovoodi_admin_site = LazyAdminSite(DovoodiAdminSite, 'dovoodi_admin') + +# from django.contrib.admin.sites import AdminSite + +# class LazyAdminSite(AdminSite): +# """Lazy wrapper that initializes the real admin site on first access""" + +# def __init__(self, site_class, name): +# # Don't call super().__init__() - avoid app registry checks at import time +# self._site_class = site_class +# self._name = name +# self._instance = None + +# def _get_instance(self): +# """Initialize the real site on first access (after Django is ready)""" +# if self._instance is None: +# self._instance = self._site_class(name=self._name) +# return self._instance + +# def __getattr__(self, name): +# """Delegate all attribute access to the real instance""" +# if name.startswith('_'): +# raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") +# return getattr(self._get_instance(), name) + +# @property +# def urls(self): +# """Required for Django URL routing""" +# return self._get_instance().urls + +# @property +# def media(self): +# """Expose the media from the real admin instance""" +# return self._get_instance().media + +# @property +# def form_class(self): +# """Expose form class""" +# return self._get_instance().form_class + +# def has_permission(self, request): +# """Check if user has admin permission""" +# return self._get_instance().has_permission(request) + +# def each_context(self, request): +# """Return context for admin templates""" +# return self._get_instance().each_context(request) + +# def get_urls(self): +# """Get admin URLs""" +# return self._get_instance().get_urls() + +# def register(self, model, admin_class=None, **options): +# """Register a model with the admin site""" +# return self._get_instance().register(model, admin_class, **options) + + +# # Create lazy instances (NO initialization at import time!) +# project_admin_site = LazyAdminSite(FormulaAdminSite, 'imam_javad_admin') +# dovoodi_admin_site = LazyAdminSite(DovoodiAdminSite, 'dovoodi_admin') # Function to replace placeholders with real instances when Django is ready def replace_placeholders_with_real_sites(): diff --git a/utils/permissions.py b/utils/permissions.py new file mode 100644 index 0000000..51d7109 --- /dev/null +++ b/utils/permissions.py @@ -0,0 +1,14 @@ +from rest_framework.permissions import BasePermission + +class IsTokenAuthenticatedOrAnonymous(BasePermission): + """ + Allow access to token-authenticated users OR anonymous users. + Useful for guest token endpoints. + """ + def has_permission(self, request, view): + # Allow if user is authenticated (including token users) + if request.user and request.user.is_authenticated: + return True + + # Allow anonymous access + return True \ No newline at end of file