-
+
+
+ {% if uses_cloud_storage %}
+
+ Using {{ storage_type }} for icon storage
+
+ {% endif %}
+
-
@@ -54,23 +44,78 @@
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/django-icon-picker/django_icon_picker/urls.py b/django-icon-picker/django_icon_picker/urls.py
index e8c7a64..bc0e1e5 100644
--- a/django-icon-picker/django_icon_picker/urls.py
+++ b/django-icon-picker/django_icon_picker/urls.py
@@ -1,6 +1,9 @@
from django.urls import path
from . import views
+app_name = 'django_icon_picker'
+
urlpatterns = [
path("download-svg/", views.download_and_save_svg, name="download_svg"),
-]
+ path("get-icon-url/", views.get_icon_url, name="get_icon_url"),
+]
\ No newline at end of file
diff --git a/django-icon-picker/django_icon_picker/views.py b/django-icon-picker/django_icon_picker/views.py
index 48585e4..2013f82 100644
--- a/django-icon-picker/django_icon_picker/views.py
+++ b/django-icon-picker/django_icon_picker/views.py
@@ -1,38 +1,129 @@
# views.py
-
-from django.http import HttpResponse
+from django.http import HttpResponse, JsonResponse
from django.conf import settings
+from django.core.files.base import ContentFile
+from django.core.files.storage import default_storage
+from django.views.decorators.csrf import csrf_exempt
+from django.contrib.auth.decorators import login_required
import requests
import os
+import uuid
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def get_icon_storage():
+ """
+ Get the storage backend for icons.
+ Uses ICON_PICKER_STORAGE setting if provided, otherwise default_storage.
+ """
+ storage_setting = getattr(settings, 'ICON_PICKER_STORAGE', None)
+
+ if storage_setting:
+ # Import and instantiate custom storage
+ if isinstance(storage_setting, str):
+ # If it's a string, import the storage class
+ from django.utils.module_loading import import_string
+ storage_class = import_string(storage_setting)
+ return storage_class()
+ else:
+ # If it's already an instance, use it
+ return storage_setting
+
+ return default_storage
+def get_icon_path(model, icon_id):
+ """Generate the storage path for an icon"""
+ base_path = getattr(settings, 'ICON_PICKER_PATH', 'icons')
+
+ # Remove leading slash for cloud storage compatibility
+ if base_path.startswith('/'):
+ base_path = base_path[1:]
+
+ return f"{base_path}/{model}/icon-{icon_id}.svg"
+
+
+@csrf_exempt
+@login_required
def download_and_save_svg(request):
+ """Download SVG from Iconify API and save using Django-storages"""
+
+ model = request.GET.get("model")
+ icon_id = request.GET.get("id")
+ svg_icon = request.GET.get("icon")
+
+ # Permission check
+ if not (request.user.is_superuser or request.user.has_perm(f"edit_{model}")):
+ return JsonResponse({"error": "Permission denied"}, status=403)
+
+ if not all([model, icon_id, svg_icon]):
+ return JsonResponse({"error": "Missing required parameters"}, status=400)
+
+ try:
+ # Construct the Iconify URL
+ color = request.GET.get("color", "").replace("#", "%23")
+ svg_url = f"https://api.iconify.design/{svg_icon}"
+ if color:
+ svg_url += f"?color={color}"
+
+ # Download the SVG content
+ response = requests.get(svg_url, timeout=10)
+ if response.status_code != 200:
+ return JsonResponse({
+ "error": f"Failed to download SVG. Status: {response.status_code}"
+ }, status=400)
+
+ # Get storage backend and file path
+ storage = get_icon_storage()
+ file_path = get_icon_path(model, icon_id)
+
+ # Save the SVG content
+ svg_content = ContentFile(response.content)
+
+ # Delete existing file if it exists
+ if storage.exists(file_path):
+ storage.delete(file_path)
+
+ # Save the new file
+ saved_path = storage.save(file_path, svg_content)
+
+ # Return the saved path (for the form field)
+ return HttpResponse(saved_path)
+
+ except requests.RequestException as e:
+ logger.error(f"Network error downloading SVG: {str(e)}")
+ return JsonResponse({"error": "Network error downloading icon"}, status=500)
+
+ except Exception as e:
+ logger.error(f"Error saving SVG: {str(e)}")
+ return JsonResponse({"error": "Error saving icon file"}, status=500)
+
+
+def get_icon_url(request):
+ """Get the URL for an icon file (useful for AJAX requests)"""
model = request.GET.get("model")
- if request.user.is_superuser or request.user.has_perm(f"edit_{model}"):
- svg_icon = request.GET.get("icon")
- color = request.GET.get("color").replace("#", "%23")
- svg_url = f"https://api.iconify.design/{svg_icon}?color={color}"
- id = request.GET.get("id")
- # Define the path where you want to save the SVG file
- save_path = getattr(settings, "ICON_PICKER_PATH")
-
- save_path = f"{save_path}/{model}"
- # Ensure the save path exists
- os.makedirs(save_path, exist_ok=True)
- # Download the SVG file
- response = requests.get(svg_url)
- if response.status_code == 200:
- # Extract the filename from the URL
- filename = os.path.basename(f"icon-{id}.svg")
- file_path = os.path.join(save_path, filename)
-
- # Save the SVG file
- with open(file_path, "wb") as f:
- f.write(response.content)
- return HttpResponse(file_path)
+ icon_id = request.GET.get("id")
+
+ if not all([model, icon_id]):
+ return JsonResponse({"error": "Missing parameters"}, status=400)
+
+ try:
+ storage = get_icon_storage()
+ file_path = get_icon_path(model, icon_id)
+
+ if storage.exists(file_path):
+ if hasattr(storage, 'url'):
+ url = storage.url(file_path)
+ else:
+ # Fallback for local storage
+ url = f"/{file_path}"
+
+ return JsonResponse({"url": url})
else:
- return HttpResponse(
- f"Failed to download SVG file. Status code: {response.reason}"
- )
- else:
- return HttpResponse("Not permitted")
+ return JsonResponse({"error": "Icon not found"}, status=404)
+
+ except Exception as e:
+ logger.error(f"Error getting icon URL: {str(e)}")
+ return JsonResponse({"error": "Error retrieving icon"}, status=500)
\ No newline at end of file
diff --git a/django-icon-picker/django_icon_picker/widgets.py b/django-icon-picker/django_icon_picker/widgets.py
index 96b77b4..5d5ba34 100644
--- a/django-icon-picker/django_icon_picker/widgets.py
+++ b/django-icon-picker/django_icon_picker/widgets.py
@@ -1,31 +1,95 @@
from django.forms import Widget
from django.conf import settings
+from django.core.files.storage import default_storage
import uuid
+import os
class IconPicker(Widget):
template_name = "django_icon_picker/icon_picker.html"
def get_settings_attr(self, context, key, attr):
+ """Safely get settings attributes"""
try:
context[key] = getattr(settings, attr)
- except:
+ except AttributeError:
pass
+ def get_storage(self):
+ """Get the storage backend for icons"""
+ storage_setting = getattr(settings, 'ICON_PICKER_STORAGE', None)
+
+ if storage_setting:
+ if isinstance(storage_setting, str):
+ from django.utils.module_loading import import_string
+ storage_class = import_string(storage_setting)
+ return storage_class()
+ else:
+ return storage_setting
+
+ return default_storage
+
+ def get_icon_url(self, value):
+ """Get the URL for the icon file"""
+ if not value:
+ return None
+
+ storage = self.get_storage()
+
+ # If value is already a URL (starts with http), return as is
+ if value.startswith(('http://', 'https://')):
+ return value
+
+ # If it's an Iconify icon (contains colon), construct Iconify URL
+ if ':' in value and not value.endswith('.svg'):
+ return f"https://api.iconify.design/{value}.svg"
+
+ # For stored files, use storage backend
+ try:
+ if hasattr(storage, 'url') and storage.exists(value):
+ return storage.url(value)
+ elif os.path.exists(value):
+ # Local file fallback
+ return f"/{value}" if not value.startswith('/') else value
+ except Exception:
+ pass
+
+ return None
+
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
+
+ # Add storage-related context
self.get_settings_attr(context, "save_path", "ICON_PICKER_PATH")
self.get_settings_attr(context, "default_color", "ICON_PICKER_COLOR")
- context.update(
- {
- "object_id": self.get_object_id(value),
- }
- )
+
+ # Add storage type information
+ storage = self.get_storage()
+ context.update({
+ "object_id": self.get_object_id(value),
+ "icon_url": self.get_icon_url(value),
+ "uses_cloud_storage": hasattr(storage, 'bucket_name') or hasattr(storage, 'azure_container'),
+ "storage_type": storage.__class__.__name__,
+ })
+
return context
def get_object_id(self, value):
+ """Extract object ID from the stored value"""
if value:
- return value.split("/")[-1].split(".")[0].replace("icon-", "")
+ # Try to extract ID from file path
+ if value.endswith('.svg'):
+ filename = os.path.basename(value)
+ if filename.startswith('icon-') and filename.endswith('.svg'):
+ return filename[5:-4] # Remove 'icon-' prefix and '.svg' suffix
+
+ # For other formats, try to extract from path
+ parts = value.split('/')
+ if len(parts) > 0:
+ last_part = parts[-1]
+ if last_part.startswith('icon-'):
+ return last_part.replace('icon-', '').replace('.svg', '')
+
return str(uuid.uuid4())
class Media:
@@ -38,4 +102,4 @@ class Media:
js = (
"https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js",
"django_icon_picker/js/icon_picker.js",
- )
+ )
\ No newline at end of file
diff --git a/django-icon-picker/setup.py b/django-icon-picker/setup.py
index 7070221..5007b76 100644
--- a/django-icon-picker/setup.py
+++ b/django-icon-picker/setup.py
@@ -23,18 +23,54 @@
"django_icon_picker": ["templates/django_icon_picker/icon_picker.html"],
},
classifiers=[
- "Development Status :: 3 - Alpha",
- "Environment :: Web Environment",
- "Framework :: Django",
- "Framework :: Django :: 3.0",
+ "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
- "Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Framework :: Django",
+ "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.0",
+ "Framework :: Django :: 4.1",
+ "Framework :: Django :: 4.2",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Software Development :: Libraries :: Python Modules",
],
-)
+ python_requires=">=3.8",
+ install_requires=[
+ "Django>=3.2,<5.0",
+ "requests>=2.25.0",
+ ],
+ extras_require={
+ "s3": ["django-storages[boto3]>=1.12.0"],
+ "gcs": ["django-storages[google]>=1.12.0"],
+ "azure": ["django-storages[azure]>=1.12.0"],
+ "all-storage": ["django-storages[boto3,google,azure]>=1.12.0"],
+ "dev": [
+ "pytest>=6.0",
+ "pytest-django>=4.0",
+ "black>=21.0",
+ "isort>=5.0",
+ "flake8>=3.8",
+ "coverage>=5.0",
+ ],
+ },
+ include_package_data=True,
+ package_data={
+ "django_icon_picker": [
+ "static/django_icon_picker/css/*.css",
+ "static/django_icon_picker/js/*.js",
+ "templates/django_icon_picker/*.html",
+ ],
+ },
+ keywords="django icons iconify storage s3 gcs azure",
+ project_urls={
+ "Bug Reports": "https://github.com/bixat/django-icon-picker/issues",
+ "Source": "https://github.com/bixat/django-icon-picker",
+ "Documentation": "https://pypi.org/project/django-icon-picker/",
+ },
+)
\ No newline at end of file
diff --git a/django_icon_picker_example/django_icon_picker_example/settings.py b/django_icon_picker_example/django_icon_picker_example/settings.py
index c330402..afcfb52 100644
--- a/django_icon_picker_example/django_icon_picker_example/settings.py
+++ b/django_icon_picker_example/django_icon_picker_example/settings.py
@@ -39,6 +39,7 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"django_icon_picker",
+ "storages",
"example",
]
@@ -123,6 +124,7 @@
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
+# Basic configuration (works with any storage backend)
ICON_PICKER_PATH = "media"
ICON_PICKER_COLOR = "#00bcc9"
@@ -130,3 +132,95 @@
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+
+# Django Settings Examples for Icon Picker with Django-storages
+
+# Example 1: AWS S3 Storage
+# pip install django-storages[boto3]
+ICON_PICKER_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
+
+# S3 Configuration
+# AWS_ACCESS_KEY_ID = 'your-access-key'
+# AWS_SECRET_ACCESS_KEY = 'your-secret-key'
+# AWS_STORAGE_BUCKET_NAME = 'your-bucket-name'
+# AWS_S3_REGION_NAME = 'us-east-1'
+# AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
+# AWS_DEFAULT_ACL = 'public-read'
+# AWS_S3_OBJECT_PARAMETERS = {
+# 'CacheControl': 'max-age=86400',
+# }
+
+# Example 2: Google Cloud Storage
+# pip install django-storages[google]
+# ICON_PICKER_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
+
+# GCS Configuration
+# GS_BUCKET_NAME = 'your-bucket-name'
+# GS_PROJECT_ID = 'your-project-id'
+# GS_DEFAULT_ACL = 'publicRead'
+# GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
+# "path/to/your/service-account-key.json"
+# )
+
+# Example 3: Azure Blob Storage
+# pip install django-storages[azure]
+# ICON_PICKER_STORAGE = 'storages.backends.azure_storage.AzureStorage'
+
+# Azure Configuration
+# AZURE_ACCOUNT_NAME = 'your-account-name'
+# AZURE_ACCOUNT_KEY = 'your-account-key'
+# AZURE_CONTAINER = 'your-container-name'
+# AZURE_CUSTOM_DOMAIN = f'{AZURE_ACCOUNT_NAME}.blob.core.windows.net'
+
+# Example 4: DigitalOcean Spaces
+# pip install django-storages[boto3]
+# ICON_PICKER_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+
+# DO Spaces Configuration
+# AWS_ACCESS_KEY_ID = 'your-spaces-key'
+# AWS_SECRET_ACCESS_KEY = 'your-spaces-secret'
+# AWS_STORAGE_BUCKET_NAME = 'your-space-name'
+# AWS_S3_ENDPOINT_URL = 'https://nyc3.digitaloceanspaces.com'
+# AWS_S3_REGION_NAME = 'nyc3'
+# AWS_DEFAULT_ACL = 'public-read'
+
+# Example 5: Custom Storage Backend
+# You can also pass a storage instance directly
+# from django.core.files.storage import FileSystemStorage
+# import os
+
+# Custom local storage with different base path
+# ICON_STORAGE = FileSystemStorage(
+# location=os.path.join(BASE_DIR, 'custom_icons'),
+# base_url='/custom_icons/'
+# )
+# ICON_PICKER_STORAGE = ICON_STORAGE
+
+# Example 6: CDN Integration (with S3 + CloudFront)
+# ICON_PICKER_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+# AWS_S3_CUSTOM_DOMAIN = 'your-cdn-domain.cloudfront.net'
+
+# Example 7: Multiple Storage Backends
+# You can even create different storage for different environments
+# import os
+# from django.conf import settings
+
+# if settings.DEBUG:
+# Use local storage in development
+# ICON_PICKER_STORAGE = None # Uses default_storage
+# else:
+# Use cloud storage in production
+# ICON_PICKER_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+
+# Security Settings for Production
+# SECURE_SSL_REDIRECT = True
+# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
+# Optional: Custom permissions
+# For icons that should be publicly accessible
+# AWS_DEFAULT_ACL = 'public-read'
+
+# For private icons (requires signed URLs)
+# AWS_DEFAULT_ACL = 'private'
+# AWS_QUERYSTRING_AUTH = True
+# AWS_QUERYSTRING_EXPIRE = 3600 # 1 hour
diff --git a/django_icon_picker_example/example/tests.py b/django_icon_picker_example/example/tests.py
index 7ce503c..a79ca8b 100644
--- a/django_icon_picker_example/example/tests.py
+++ b/django_icon_picker_example/example/tests.py
@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
# Create your tests here.
diff --git a/django_icon_picker_example/example/views.py b/django_icon_picker_example/example/views.py
index 91ea44a..fd0e044 100644
--- a/django_icon_picker_example/example/views.py
+++ b/django_icon_picker_example/example/views.py
@@ -1,3 +1,3 @@
-from django.shortcuts import render
+# from django.shortcuts import render
# Create your views here.