plugin distribution system
This commit is contained in:
658
backend/igny8_core/plugins/views.py
Normal file
658
backend/igny8_core/plugins/views.py
Normal file
@@ -0,0 +1,658 @@
|
||||
"""
|
||||
Plugin Distribution System Views
|
||||
|
||||
API endpoints for plugin distribution, updates, and management.
|
||||
"""
|
||||
import logging
|
||||
from django.http import FileResponse, Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import Plugin, PluginVersion, PluginInstallation, PluginDownload
|
||||
from .serializers import (
|
||||
PluginSerializer,
|
||||
PluginDetailSerializer,
|
||||
PluginVersionSerializer,
|
||||
PluginInfoSerializer,
|
||||
CheckUpdateSerializer,
|
||||
RegisterInstallationSerializer,
|
||||
HealthCheckSerializer,
|
||||
PluginInstallationSerializer,
|
||||
AdminPluginVersionCreateSerializer,
|
||||
AdminPluginVersionUploadSerializer,
|
||||
)
|
||||
from .utils import get_plugin_file_path, parse_version_to_code
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Public Endpoints (No Auth Required)
|
||||
# ============================================================================
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def download_plugin(request, slug):
|
||||
"""
|
||||
Download the latest version of a plugin.
|
||||
|
||||
GET /api/plugins/{slug}/download/
|
||||
|
||||
Returns the ZIP file for the latest released version.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||
latest = plugin.get_latest_version()
|
||||
|
||||
if not latest:
|
||||
return Response(
|
||||
{'error': 'No version available for download'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
file_path = get_plugin_file_path(plugin.platform, latest.file_path)
|
||||
|
||||
if not file_path or not file_path.exists():
|
||||
logger.error(f"Plugin file not found: {latest.file_path}")
|
||||
return Response(
|
||||
{'error': 'Plugin file not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Track download
|
||||
PluginDownload.objects.create(
|
||||
plugin=plugin,
|
||||
version=latest,
|
||||
ip_address=get_client_ip(request),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
|
||||
download_type='manual',
|
||||
)
|
||||
|
||||
response = FileResponse(
|
||||
open(file_path, 'rb'),
|
||||
content_type='application/zip'
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename="{latest.file_path}"'
|
||||
response['Content-Length'] = latest.file_size
|
||||
return response
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def check_update(request, slug):
|
||||
"""
|
||||
Check if an update is available for a plugin.
|
||||
|
||||
GET /api/plugins/{slug}/check-update/?current_version=1.0.0
|
||||
|
||||
Called by installed plugins to check for updates.
|
||||
Returns update availability and download URL if available.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||
current_version = request.query_params.get('current_version', '0.0.0')
|
||||
current_code = parse_version_to_code(current_version)
|
||||
|
||||
latest = plugin.get_latest_version()
|
||||
|
||||
if not latest:
|
||||
return Response({
|
||||
'update_available': False,
|
||||
'current_version': current_version,
|
||||
'latest_version': None,
|
||||
'latest_version_code': None,
|
||||
'download_url': None,
|
||||
'changelog': None,
|
||||
'info_url': None,
|
||||
'force_update': False,
|
||||
'checksum': None,
|
||||
})
|
||||
|
||||
update_available = latest.version_code > current_code
|
||||
|
||||
# Build download URL
|
||||
download_url = None
|
||||
if update_available:
|
||||
download_url = request.build_absolute_uri(f'/api/plugins/{slug}/download/')
|
||||
|
||||
response_data = {
|
||||
'update_available': update_available,
|
||||
'current_version': current_version,
|
||||
'latest_version': latest.version if update_available else None,
|
||||
'latest_version_code': latest.version_code if update_available else None,
|
||||
'download_url': download_url,
|
||||
'changelog': latest.changelog if update_available else None,
|
||||
'info_url': request.build_absolute_uri(f'/api/plugins/{slug}/info/'),
|
||||
'force_update': latest.force_update if update_available else False,
|
||||
'checksum': latest.checksum if update_available else None,
|
||||
}
|
||||
|
||||
serializer = CheckUpdateSerializer(response_data)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def plugin_info(request, slug):
|
||||
"""
|
||||
Get plugin information in WordPress-compatible format.
|
||||
|
||||
GET /api/plugins/{slug}/info/
|
||||
|
||||
Returns detailed plugin information for WordPress plugin info modal.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||
latest = plugin.get_latest_version()
|
||||
|
||||
if not latest:
|
||||
return Response(
|
||||
{'error': 'No version available'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
info_data = {
|
||||
'name': plugin.name,
|
||||
'slug': plugin.slug,
|
||||
'version': latest.version,
|
||||
'author': 'IGNY8',
|
||||
'homepage': plugin.homepage_url or 'https://igny8.com',
|
||||
'description': plugin.description,
|
||||
'changelog': latest.changelog,
|
||||
'download_url': request.build_absolute_uri(f'/api/plugins/{slug}/download/'),
|
||||
'file_size': latest.file_size,
|
||||
'requires_php': latest.min_php_version,
|
||||
'tested_wp': '6.7',
|
||||
}
|
||||
|
||||
serializer = PluginInfoSerializer(info_data)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def get_latest_plugin(request, platform):
|
||||
"""
|
||||
Get the latest plugin for a platform.
|
||||
|
||||
GET /api/plugins/{platform}/latest/
|
||||
|
||||
Returns plugin info and download URL for the specified platform.
|
||||
"""
|
||||
valid_platforms = ['wordpress', 'shopify', 'custom']
|
||||
if platform not in valid_platforms:
|
||||
return Response(
|
||||
{'error': f'Invalid platform. Must be one of: {valid_platforms}'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
plugin = Plugin.objects.filter(platform=platform, is_active=True).first()
|
||||
|
||||
if not plugin:
|
||||
return Response(
|
||||
{'error': f'No plugin available for platform: {platform}'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
latest = plugin.get_latest_version()
|
||||
|
||||
if not latest:
|
||||
return Response(
|
||||
{'error': 'No version available'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
return Response({
|
||||
'name': plugin.name,
|
||||
'slug': plugin.slug,
|
||||
'version': latest.version,
|
||||
'download_url': request.build_absolute_uri(f'/api/plugins/{plugin.slug}/download/'),
|
||||
'file_size': latest.file_size,
|
||||
'platform': plugin.platform,
|
||||
'description': plugin.description,
|
||||
'changelog': latest.changelog,
|
||||
'released_at': latest.released_at,
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Authenticated Endpoints
|
||||
# ============================================================================
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def register_installation(request, slug):
|
||||
"""
|
||||
Register a plugin installation on a site.
|
||||
|
||||
POST /api/plugins/{slug}/register-installation/
|
||||
Body: { "site_id": 123, "version": "1.0.0" }
|
||||
|
||||
Called after plugin is activated on a site.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||
serializer = RegisterInstallationSerializer(data=request.data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
from igny8_core.auth.models import Site
|
||||
site_id = serializer.validated_data['site_id']
|
||||
version_str = serializer.validated_data['version']
|
||||
|
||||
# Verify site belongs to user's account
|
||||
site = get_object_or_404(Site, id=site_id)
|
||||
|
||||
if hasattr(request, 'account') and site.account_id != request.account.id:
|
||||
return Response(
|
||||
{'error': 'Site does not belong to your account'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
# Find version record
|
||||
try:
|
||||
version = PluginVersion.objects.get(plugin=plugin, version=version_str)
|
||||
except PluginVersion.DoesNotExist:
|
||||
version = None
|
||||
|
||||
# Create or update installation record
|
||||
installation, created = PluginInstallation.objects.update_or_create(
|
||||
site=site,
|
||||
plugin=plugin,
|
||||
defaults={
|
||||
'current_version': version,
|
||||
'is_active': True,
|
||||
'last_health_check': timezone.now(),
|
||||
'health_status': 'healthy',
|
||||
}
|
||||
)
|
||||
|
||||
action = 'registered' if created else 'updated'
|
||||
logger.info(f"Plugin installation {action}: {plugin.name} v{version_str} on site {site.name}")
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': f'Installation {action} successfully',
|
||||
'installation_id': installation.id,
|
||||
})
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def health_check(request, slug):
|
||||
"""
|
||||
Report plugin health status.
|
||||
|
||||
POST /api/plugins/{slug}/health-check/
|
||||
Body: { "site_id": 123, "version": "1.0.0", "status": "active" }
|
||||
|
||||
Called periodically by installed plugins to report status.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||
serializer = HealthCheckSerializer(data=request.data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
from igny8_core.auth.models import Site
|
||||
site_id = serializer.validated_data['site_id']
|
||||
version_str = serializer.validated_data['version']
|
||||
plugin_status = serializer.validated_data['status']
|
||||
|
||||
site = get_object_or_404(Site, id=site_id)
|
||||
|
||||
try:
|
||||
installation = PluginInstallation.objects.get(site=site, plugin=plugin)
|
||||
except PluginInstallation.DoesNotExist:
|
||||
# Auto-register if not exists
|
||||
try:
|
||||
version = PluginVersion.objects.get(plugin=plugin, version=version_str)
|
||||
except PluginVersion.DoesNotExist:
|
||||
version = None
|
||||
|
||||
installation = PluginInstallation.objects.create(
|
||||
site=site,
|
||||
plugin=plugin,
|
||||
current_version=version,
|
||||
is_active=plugin_status == 'active',
|
||||
)
|
||||
|
||||
# Update installation status
|
||||
installation.last_health_check = timezone.now()
|
||||
installation.is_active = plugin_status == 'active'
|
||||
|
||||
if plugin_status == 'error':
|
||||
installation.health_status = 'error'
|
||||
else:
|
||||
installation.update_health_status()
|
||||
|
||||
installation.save()
|
||||
|
||||
# Check if update is available
|
||||
update_available = installation.check_for_update()
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'health_status': installation.health_status,
|
||||
'update_available': update_available is not None,
|
||||
'latest_version': update_available.version if update_available else None,
|
||||
})
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def track_download(request, slug):
|
||||
"""
|
||||
Track a plugin download for analytics.
|
||||
|
||||
POST /api/plugins/{slug}/track-download/
|
||||
|
||||
Called before redirecting to download URL.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||
latest = plugin.get_latest_version()
|
||||
|
||||
if not latest:
|
||||
return Response(
|
||||
{'error': 'No version available'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Get site if provided
|
||||
site = None
|
||||
site_id = request.data.get('site_id')
|
||||
if site_id:
|
||||
from igny8_core.auth.models import Site
|
||||
try:
|
||||
site = Site.objects.get(id=site_id)
|
||||
except Site.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Get account from request
|
||||
account = getattr(request, 'account', None)
|
||||
|
||||
# Track download
|
||||
PluginDownload.objects.create(
|
||||
plugin=plugin,
|
||||
version=latest,
|
||||
site=site,
|
||||
account=account,
|
||||
ip_address=get_client_ip(request),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
|
||||
download_type='manual',
|
||||
)
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'download_url': request.build_absolute_uri(f'/api/plugins/{slug}/download/'),
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Endpoints
|
||||
# ============================================================================
|
||||
|
||||
class AdminPluginListView(APIView):
|
||||
"""List all plugins and their versions (admin)."""
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def get(self, request):
|
||||
plugins = Plugin.objects.prefetch_related('versions').all()
|
||||
serializer = PluginDetailSerializer(plugins, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
"""Create a new plugin."""
|
||||
serializer = PluginSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
plugin = serializer.save()
|
||||
return Response(
|
||||
PluginSerializer(plugin).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class AdminPluginVersionsView(APIView):
|
||||
"""Manage plugin versions (admin)."""
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def get(self, request, slug):
|
||||
"""List all versions for a plugin."""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
versions = plugin.versions.all()
|
||||
serializer = PluginVersionSerializer(versions, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, slug):
|
||||
"""Create a new version for a plugin."""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
serializer = AdminPluginVersionCreateSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
# Check if version already exists
|
||||
version_str = serializer.validated_data['version']
|
||||
if PluginVersion.objects.filter(plugin=plugin, version=version_str).exists():
|
||||
return Response(
|
||||
{'error': f'Version {version_str} already exists'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
version = serializer.save(plugin=plugin)
|
||||
return Response(
|
||||
PluginVersionSerializer(version).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class AdminPluginVersionDetailView(APIView):
|
||||
"""Manage a specific plugin version (admin)."""
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def get(self, request, slug, version):
|
||||
"""Get version details."""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
version_obj = get_object_or_404(PluginVersion, plugin=plugin, version=version)
|
||||
serializer = PluginVersionSerializer(version_obj)
|
||||
return Response(serializer.data)
|
||||
|
||||
def patch(self, request, slug, version):
|
||||
"""Update version details."""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
version_obj = get_object_or_404(PluginVersion, plugin=plugin, version=version)
|
||||
|
||||
serializer = PluginVersionSerializer(version_obj, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, slug, version):
|
||||
"""Delete a version (only drafts)."""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
version_obj = get_object_or_404(PluginVersion, plugin=plugin, version=version)
|
||||
|
||||
if version_obj.status not in ['draft', 'testing']:
|
||||
return Response(
|
||||
{'error': 'Can only delete draft or testing versions'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
version_obj.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAdminUser])
|
||||
def admin_release_version(request, slug, version):
|
||||
"""
|
||||
Release a plugin version.
|
||||
|
||||
POST /api/admin/plugins/{slug}/versions/{version}/release/
|
||||
|
||||
Makes the version available for download.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
version_obj = get_object_or_404(PluginVersion, plugin=plugin, version=version)
|
||||
|
||||
if version_obj.status in ['released', 'update_ready']:
|
||||
return Response(
|
||||
{'error': 'Version is already released'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
if not version_obj.file_path:
|
||||
return Response(
|
||||
{'error': 'No file uploaded for this version'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
version_obj.release()
|
||||
|
||||
logger.info(f"Released plugin version: {plugin.name} v{version}")
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': f'Version {version} released successfully',
|
||||
'released_at': version_obj.released_at,
|
||||
})
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAdminUser])
|
||||
def admin_push_update(request, slug, version):
|
||||
"""
|
||||
Push update to all installed sites.
|
||||
|
||||
POST /api/admin/plugins/{slug}/versions/{version}/push-update/
|
||||
|
||||
Sets status to 'update_ready' and notifies installations.
|
||||
"""
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
version_obj = get_object_or_404(PluginVersion, plugin=plugin, version=version)
|
||||
|
||||
if version_obj.status not in ['released', 'update_ready']:
|
||||
return Response(
|
||||
{'error': 'Version must be released before pushing update'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Mark as update ready
|
||||
version_obj.status = 'update_ready'
|
||||
version_obj.save(update_fields=['status'])
|
||||
|
||||
# Update all installations with pending update
|
||||
installations = PluginInstallation.objects.filter(
|
||||
plugin=plugin,
|
||||
is_active=True
|
||||
).exclude(current_version=version_obj)
|
||||
|
||||
updated_count = installations.update(
|
||||
pending_update=version_obj,
|
||||
update_notified_at=timezone.now()
|
||||
)
|
||||
|
||||
logger.info(f"Pushed update {plugin.name} v{version} to {updated_count} installations")
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': f'Update pushed to {updated_count} installations',
|
||||
'installations_notified': updated_count,
|
||||
})
|
||||
|
||||
|
||||
class AdminPluginInstallationsView(APIView):
|
||||
"""View plugin installations (admin)."""
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def get(self, request, slug=None):
|
||||
"""List installations, optionally filtered by plugin."""
|
||||
if slug:
|
||||
plugin = get_object_or_404(Plugin, slug=slug)
|
||||
installations = PluginInstallation.objects.filter(plugin=plugin)
|
||||
else:
|
||||
installations = PluginInstallation.objects.all()
|
||||
|
||||
installations = installations.select_related(
|
||||
'site', 'plugin', 'current_version', 'pending_update'
|
||||
)
|
||||
|
||||
serializer = PluginInstallationSerializer(installations, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class AdminPluginStatsView(APIView):
|
||||
"""Get plugin statistics (admin)."""
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def get(self, request, slug=None):
|
||||
"""Get download and installation statistics."""
|
||||
from django.db.models import Count
|
||||
from django.db.models.functions import TruncDate
|
||||
|
||||
plugins_qs = Plugin.objects.all()
|
||||
if slug:
|
||||
plugins_qs = plugins_qs.filter(slug=slug)
|
||||
|
||||
stats = []
|
||||
for plugin in plugins_qs:
|
||||
# Version distribution
|
||||
version_dist = PluginInstallation.objects.filter(
|
||||
plugin=plugin,
|
||||
is_active=True
|
||||
).values('current_version__version').annotate(
|
||||
count=Count('id')
|
||||
).order_by('-count')
|
||||
|
||||
# Download counts by day (last 30 days)
|
||||
from datetime import timedelta
|
||||
thirty_days_ago = timezone.now() - timedelta(days=30)
|
||||
|
||||
daily_downloads = PluginDownload.objects.filter(
|
||||
plugin=plugin,
|
||||
created_at__gte=thirty_days_ago
|
||||
).annotate(
|
||||
date=TruncDate('created_at')
|
||||
).values('date').annotate(
|
||||
count=Count('id')
|
||||
).order_by('date')
|
||||
|
||||
# Health status distribution
|
||||
health_dist = PluginInstallation.objects.filter(
|
||||
plugin=plugin
|
||||
).values('health_status').annotate(
|
||||
count=Count('id')
|
||||
)
|
||||
|
||||
stats.append({
|
||||
'plugin': plugin.slug,
|
||||
'name': plugin.name,
|
||||
'total_downloads': plugin.get_download_count(),
|
||||
'active_installations': PluginInstallation.objects.filter(
|
||||
plugin=plugin, is_active=True
|
||||
).count(),
|
||||
'version_distribution': list(version_dist),
|
||||
'daily_downloads': list(daily_downloads),
|
||||
'health_distribution': list(health_dist),
|
||||
})
|
||||
|
||||
if slug and stats:
|
||||
return Response(stats[0])
|
||||
return Response(stats)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
def get_client_ip(request):
|
||||
"""Extract client IP from request."""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0].strip()
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
Reference in New Issue
Block a user