added legacy flavor + image caching
No files matched your search
@@ -1,144 +1,95 @@
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files import File
|
||||
from tempfile import NamedTemporaryFile
|
||||
from django.conf import settings
|
||||
from api.models import Category, Flavor
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds legacy flavors that are no longer in the official API or are old versions'
|
||||
help = 'Adds legacy flavors from JSON files in the legacy/ folder'
|
||||
|
||||
def download_image(self, url, flavor_name):
|
||||
if not url:
|
||||
return None
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
img_temp = NamedTemporaryFile(delete=True)
|
||||
img_temp.write(response.read())
|
||||
img_temp.flush()
|
||||
|
||||
ext = url.split('.')[-1].split('?')[0]
|
||||
if len(ext) > 4: ext = 'png'
|
||||
filename = f"legacy_{flavor_name.replace(' ', '_').lower()}.{ext}"
|
||||
return File(img_temp, name=filename)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Could not download image for {flavor_name}: {e}"))
|
||||
return None
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Ensure categories exist
|
||||
energy = Category.objects.get(slug='energy')
|
||||
hydration = Category.objects.get(slug='hydration')
|
||||
iced_tea = Category.objects.get(slug='iced-tea')
|
||||
# 1. Remove old legacy flavors (ones without external_id or marked as legacy)
|
||||
# To be safe, we only remove those marked as legacy or with no external_id
|
||||
removed_count, _ = Flavor.objects.filter(is_legacy=True).delete()
|
||||
Flavor.objects.filter(external_id__isnull=True).delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Removed {removed_count} old legacy flavors."))
|
||||
|
||||
legacy_data = [
|
||||
{
|
||||
"Name": "Hitschies Saure Drachenzunge (Legacy)",
|
||||
"Geschmack": "Blaue Himbeere",
|
||||
"Beschreibung": "Eine legendäre Kollaboration mit Hitschies. Schmeckt wie die blauen sauren Drachenzungen – süß, fruchtig und sauer.",
|
||||
"Status": "Limited Edition / Vaulted",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/Holy_Energy_Hitschies_Sour_Dragon_Tongue_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Hitschies Saure Drachenzunge Rot (Legacy)",
|
||||
"Geschmack": "Erdbeere & Kirsche",
|
||||
"Beschreibung": "Die rote Variante der Hitschies-Kollaboration. Fruchtiger Mix aus roten Beeren, angelehnt an die roten Drachenzungen.",
|
||||
"Status": "Limited Edition",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/Holy_Energy_Hitschies_Sour_Dragon_Tongue_Red_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Lion's Lemonade (Legacy)",
|
||||
"Geschmack": "Mango & Kiwi",
|
||||
"Beschreibung": "Einer der beliebtesten Klassiker. Ein tropischer, fruchtiger Mix für maximalen Fokus. Oft von Fans zurückgefordert.",
|
||||
"Status": "Vaulted",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Lions_Lemonade_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Lollipop Lovebird (Legacy)",
|
||||
"Geschmack": "Kirsch-Lolli",
|
||||
"Beschreibung": "Erinnert an die klassischen roten Kirsch-Lollies aus der Kindheit. Sehr süß und intensiv.",
|
||||
"Status": "Limited Edition",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Lollipop_Lovebird_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Frosty Fox (Legacy)",
|
||||
"Geschmack": "Eisbonbon",
|
||||
"Beschreibung": "Ein kühler, süßer Geschmack, der an die klassischen Eisbonbons erinnert. Erfrischend mit einer 'kühlen' Note.",
|
||||
"Status": "Limited Edition",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Frosty_Fox_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Bubble Gum Butterfly (Legacy)",
|
||||
"Geschmack": "Kaugummi",
|
||||
"Beschreibung": "Der typische rosa Kaugummi-Geschmack (Bubblegum). Sehr süß und nostalgisch.",
|
||||
"Status": "Limited Edition",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Bubblegum_Butterfly_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Woodruff Wolf (Legacy)",
|
||||
"Geschmack": "Waldmeister",
|
||||
"Beschreibung": "Ein traditioneller deutscher Geschmack. Süß-würzig, wie Wackelpudding oder Waldmeister-Sirup.",
|
||||
"Status": "Limited Edition",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Woodruff_Wolf_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Baked Apple Boar (Legacy)",
|
||||
"Geschmack": "Bratapfel",
|
||||
"Beschreibung": "Eine winterliche Sonderedition mit warmen Noten von Apfel und Zimt.",
|
||||
"Status": "Seasonal / Vaulted",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Baked_Apple_Boar_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Kölle Kamelle (Legacy)",
|
||||
"Geschmack": "Fruchtbonbons (Karnevalsmischung)",
|
||||
"Beschreibung": "Spezialedition zum Kölner Karneval. Schmeckt nach einem Mix aus verschiedenen Fruchtbonbons ('Kamelle').",
|
||||
"Status": "Limited Edition",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Koelle_Kamelle_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Strawberry Shark (Legacy)",
|
||||
"Geschmack": "Erdbeere & Mandarine",
|
||||
"Beschreibung": "Ein fruchtiger Mix aus süßer Erdbeere und spritziger Mandarine. Teil des ursprünglichen 'Squads'.",
|
||||
"Status": "Discontinued",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Strawberry_Shark_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Blueberry Bear (Legacy)",
|
||||
"Geschmack": "Blaubeere & Kokosnuss",
|
||||
"Beschreibung": "Eine weiche Beeren-Note mit einem exotischen Kokos-Abgang.",
|
||||
"Status": "Discontinued",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Blueberry_Bear_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Thai Lime Toucan (Legacy)",
|
||||
"Geschmack": "Kaffir-Limette",
|
||||
"Beschreibung": "Sehr aromatisch und zesty. Unterscheidet sich von normaler Zitrone durch die herbe Kaffir-Note.",
|
||||
"Status": "Discontinued",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Thai_Lime_Toucan_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Cactus Camel (Legacy)",
|
||||
"Geschmack": "Kaktusfeige",
|
||||
"Beschreibung": "Erfrischend, leicht melonenartig und nicht zu süß. Ein sehr eigener, beliebter Geschmack.",
|
||||
"Status": "Discontinued",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Cactus_Camel_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Apple Alligator (Legacy)",
|
||||
"Geschmack": "Grüner Apfel",
|
||||
"Beschreibung": "Knackiger, saurer Apfelgeschmack. Sehr erfrischend.",
|
||||
"Status": "Discontinued",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Apple_Alligator_Tub_720x.png"
|
||||
},
|
||||
{
|
||||
"Name": "Cherry Cheetah (Legacy)",
|
||||
"Geschmack": "Kirsche",
|
||||
"Beschreibung": "Klassischer, fruchtiger Kirschgeschmack.",
|
||||
"Status": "Discontinued",
|
||||
"Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Cherry_Cheetah_Tub_720x.png"
|
||||
}
|
||||
]
|
||||
# Ensure main categories exist
|
||||
energy = Category.objects.get_or_create(name='Energy', defaults={'slug': 'energy'})[0]
|
||||
hydration = Category.objects.get_or_create(name='Hydration', defaults={'slug': 'hydration'})[0]
|
||||
iced_tea = Category.objects.get_or_create(name='Iced Tea', defaults={'slug': 'iced-tea'})[0]
|
||||
packs = Category.objects.get_or_create(name='Packs and other', defaults={'slug': 'packs-and-other'})[0]
|
||||
|
||||
for data in legacy_data:
|
||||
full_description = f"**Geschmack:** {data['Geschmack']}\n\n{data['Beschreibung']}\n\n**Status:** {data['Status']}"
|
||||
legacy_dir = os.path.join(settings.BASE_DIR, '..', 'legacy')
|
||||
if not os.path.exists(legacy_dir):
|
||||
self.stdout.write(self.style.ERROR(f"Legacy directory not found at {legacy_dir}"))
|
||||
return
|
||||
|
||||
json_files = [f for f in os.listdir(legacy_dir) if f.endswith('.json')]
|
||||
|
||||
created_count = 0
|
||||
|
||||
for json_file in json_files:
|
||||
file_path = os.path.join(legacy_dir, json_file)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data_list = json.load(f)
|
||||
|
||||
cat = energy # Default
|
||||
if "hydration" in data['Name'].lower() or "hydration" in data['Beschreibung'].lower():
|
||||
cat = hydration
|
||||
elif "iced tea" in data['Name'].lower() or "eistee" in data['Name'].lower():
|
||||
cat = iced_tea
|
||||
for data in data_list:
|
||||
name = data.get('Name')
|
||||
desc = data.get('Beschreibung')
|
||||
taste = data.get('Geschmack')
|
||||
status_text = data.get('Status')
|
||||
image_url = data.get('Bild_URL')
|
||||
|
||||
flavor, created = Flavor.objects.update_or_create(
|
||||
name=data['Name'],
|
||||
defaults={
|
||||
'category': cat,
|
||||
'description': full_description,
|
||||
'image_url': data['Bild_URL'],
|
||||
'is_available': False,
|
||||
'external_id': None
|
||||
}
|
||||
)
|
||||
full_desc = f"**Geschmack:** {taste}\n\n{desc}\n\n**Status:** {status_text}"
|
||||
|
||||
# Determine category
|
||||
cat = energy
|
||||
if "hydration" in name.lower() or "hydration" in desc.lower():
|
||||
cat = hydration
|
||||
elif "iced tea" in name.lower() or "eistee" in name.lower():
|
||||
cat = iced_tea
|
||||
elif any(k in name.lower() for k in ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'shaker', 'starter']):
|
||||
cat = packs
|
||||
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(f"Added legacy flavor: {data['Name']}"))
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f"Updated legacy flavor: {data['Name']}"))
|
||||
flavor = Flavor(
|
||||
name=name,
|
||||
category=cat,
|
||||
description=full_desc,
|
||||
image_url=image_url,
|
||||
is_available=False,
|
||||
is_legacy=True,
|
||||
external_id=None
|
||||
)
|
||||
|
||||
if image_url:
|
||||
img_file = self.download_image(image_url, name)
|
||||
if img_file:
|
||||
flavor.image.save(img_file.name, img_file, save=False)
|
||||
|
||||
flavor.save()
|
||||
created_count += 1
|
||||
self.stdout.write(f"Added legacy flavor: {name}")
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Finished! Created {created_count} legacy flavors from JSON."))
|
||||
@@ -1,8 +1,10 @@
|
||||
import json
|
||||
import urllib.request
|
||||
import re
|
||||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.text import slugify
|
||||
from django.core.files import File
|
||||
from tempfile import NamedTemporaryFile
|
||||
from api.models import Category, Flavor
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,12 +17,34 @@ class Command(BaseCommand):
|
||||
cleantext = re.sub(cleanr, '', raw_html)
|
||||
return cleantext.strip()
|
||||
|
||||
def download_image(self, url, flavor_name):
|
||||
if not url:
|
||||
return None
|
||||
try:
|
||||
# Simple caching: check if we already have a local image for this URL
|
||||
# (In a real app, you might compare filenames or hashes)
|
||||
img_temp = NamedTemporaryFile(delete=True)
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
img_temp.write(response.read())
|
||||
img_temp.flush()
|
||||
|
||||
# Use extension from URL or default to .png
|
||||
ext = url.split('.')[-1].split('?')[0]
|
||||
if len(ext) > 4: ext = 'png'
|
||||
filename = f"{flavor_name.replace(' ', '_').lower()}.{ext}"
|
||||
return File(img_temp, name=filename)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f"Could not download image for {flavor_name}: {e}"))
|
||||
return None
|
||||
|
||||
def handle(self, *args, **options):
|
||||
url = "https://weareholy.com/products.json?limit=250"
|
||||
self.stdout.write(f"Fetching from {url}...")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url) as response:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Failed to fetch data: {e}"))
|
||||
@@ -54,7 +78,6 @@ class Command(BaseCommand):
|
||||
tags_lower = [t.lower() for t in tags]
|
||||
title_lower = title.lower()
|
||||
|
||||
# Check for Packs/Bundles/Samples
|
||||
is_pack = False
|
||||
pack_keywords = ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'starter', 'collection', 'probier']
|
||||
if any(k in title_lower for k in pack_keywords):
|
||||
@@ -62,7 +85,6 @@ class Command(BaseCommand):
|
||||
if 'shaker' in title_lower or 'merch' in tags_lower:
|
||||
is_pack = True
|
||||
|
||||
# Determine Category
|
||||
if is_pack:
|
||||
category = cat_map['packs']
|
||||
elif 'holy energy' in tags_lower or 'energy' in tags_lower:
|
||||
@@ -80,52 +102,59 @@ class Command(BaseCommand):
|
||||
else:
|
||||
continue
|
||||
|
||||
# Check availability
|
||||
variants = p.get('variants', [])
|
||||
is_available = any(v.get('available') for v in variants)
|
||||
|
||||
# Image
|
||||
images = p.get('images', [])
|
||||
image_url = images[0].get('src') if images else None
|
||||
|
||||
# Description
|
||||
description = self.clean_html(p.get('body_html', ''))
|
||||
|
||||
# Shop URL
|
||||
handle = p.get('handle')
|
||||
shop_url = f"https://weareholy.com/products/{handle}" if handle else None
|
||||
|
||||
# Create or update
|
||||
flavor = Flavor.objects.filter(external_id=p['id']).first()
|
||||
if not flavor:
|
||||
flavor = Flavor.objects.filter(name=title).first()
|
||||
flavor = Flavor.objects.filter(name=title, is_legacy=False).first()
|
||||
|
||||
if flavor:
|
||||
flavor.external_id = p['id']
|
||||
flavor.name = title
|
||||
flavor.category = category
|
||||
flavor.description = description
|
||||
flavor.image_url = image_url
|
||||
flavor.shop_url = shop_url
|
||||
flavor.is_available = is_available
|
||||
flavor.is_legacy = False # API flavors are not legacy
|
||||
|
||||
# Cache image if URL changed or local image missing
|
||||
if image_url and (flavor.image_url != image_url or not flavor.image):
|
||||
img_file = self.download_image(image_url, title)
|
||||
if img_file:
|
||||
flavor.image.save(img_file.name, img_file, save=False)
|
||||
|
||||
flavor.image_url = image_url
|
||||
flavor.save()
|
||||
updated_count += 1
|
||||
else:
|
||||
Flavor.objects.create(
|
||||
new_flavor = Flavor(
|
||||
external_id=p['id'],
|
||||
name=title,
|
||||
category=category,
|
||||
description=description,
|
||||
image_url=image_url,
|
||||
shop_url=shop_url,
|
||||
is_available=is_available
|
||||
is_available=is_available,
|
||||
is_legacy=False
|
||||
)
|
||||
if image_url:
|
||||
img_file = self.download_image(image_url, title)
|
||||
if img_file:
|
||||
new_flavor.image.save(img_file.name, img_file, save=False)
|
||||
new_flavor.save()
|
||||
created_count += 1
|
||||
|
||||
synced_external_ids.append(p['id'])
|
||||
|
||||
# Mark flavors NOT in the sync as unavailable (only for those that have an external_id)
|
||||
# This keeps manually added flavors (with external_id=None) as they are.
|
||||
# These are discontinued API flavors.
|
||||
discontinued_count = Flavor.objects.filter(external_id__isnull=False).exclude(external_id__in=synced_external_ids).update(is_available=False)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Finished! Created: {created_count}, Updated: {updated_count}, Marked Unavailable: {discontinued_count}"))
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.2 on 2026-02-16 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0006_reply'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='flavor',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='flavors/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='flavor',
|
||||
name='is_legacy',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='flavor',
|
||||
name='image_url',
|
||||
field=models.URLField(blank=True, max_length=1000, null=True),
|
||||
),
|
||||
]
|
||||
@@ -31,9 +31,11 @@ class Flavor(models.Model):
|
||||
category = models.ForeignKey(Category, related_name='flavors', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
image_url = models.URLField(max_length=500, blank=True, null=True)
|
||||
image_url = models.URLField(max_length=1000, blank=True, null=True)
|
||||
image = models.ImageField(upload_to='flavors/', blank=True, null=True)
|
||||
shop_url = models.URLField(max_length=500, blank=True, null=True)
|
||||
is_available = models.BooleanField(default=True)
|
||||
is_legacy = models.BooleanField(default=False)
|
||||
external_id = models.BigIntegerField(unique=True, null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -47,6 +49,12 @@ class Flavor(models.Model):
|
||||
return sum(r.score for r in ratings) / ratings.count()
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def cached_image_url(self):
|
||||
if self.image:
|
||||
return self.image.url
|
||||
return self.image_url
|
||||
|
||||
class Rating(models.Model):
|
||||
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
|
||||
flavor = models.ForeignKey(Flavor, related_name='ratings', on_delete=models.CASCADE)
|
||||
|
||||
@@ -22,23 +22,40 @@ class ReplySerializer(serializers.ModelSerializer):
|
||||
class RatingSerializer(serializers.ModelSerializer):
|
||||
user = serializers.StringRelatedField(read_only=True)
|
||||
flavor_name = serializers.CharField(source='flavor.name', read_only=True)
|
||||
flavor_image = serializers.CharField(source='flavor.image_url', read_only=True)
|
||||
flavor_image = serializers.SerializerMethodField()
|
||||
replies = ReplySerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Rating
|
||||
fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at', 'replies']
|
||||
|
||||
def get_flavor_image(self, obj):
|
||||
if obj.flavor.image:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
return request.build_absolute_uri(obj.flavor.image.url)
|
||||
return obj.flavor.image.url
|
||||
return obj.flavor.image_url
|
||||
|
||||
class FlavorSerializer(serializers.ModelSerializer):
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
category_slug = serializers.CharField(source='category.slug', read_only=True)
|
||||
average_rating = serializers.FloatField(read_only=True)
|
||||
user_rating = serializers.SerializerMethodField()
|
||||
ratings = RatingSerializer(many=True, read_only=True)
|
||||
image_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Flavor
|
||||
fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'shop_url']
|
||||
fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'is_legacy', 'shop_url']
|
||||
|
||||
def get_image_url(self, obj):
|
||||
if obj.image:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
return request.build_absolute_uri(obj.image.url)
|
||||
return obj.image.url
|
||||
return obj.image_url
|
||||
|
||||
def get_user_rating(self, obj):
|
||||
request = self.context.get('request')
|
||||
|
||||
@@ -130,6 +130,9 @@ STATIC_URL = 'static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# --- Reverse Proxy Configuration ---
|
||||
# These settings are crucial for running behind a proxy like Traefik
|
||||
USE_X_FORWARDED_HOST = True
|
||||
|
||||
@@ -2,6 +2,8 @@ from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
@@ -11,4 +13,4 @@ urlpatterns = [
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
]
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 616 KiB |
|
After Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 850 KiB |
|
After Width: | Height: | Size: 630 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 665 KiB |
|
After Width: | Height: | Size: 807 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 705 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 581 KiB |
|
After Width: | Height: | Size: 581 KiB |
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 827 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 11 MiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 449 KiB |
|
After Width: | Height: | Size: 7.1 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 517 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 5.7 MiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 711 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 181 KiB |