generated from bisco/codex-bootstrap
feat(shows): add uploaded show images
This commit is contained in:
@@ -116,6 +116,8 @@ if "test" in sys.argv:
|
|||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -16,3 +18,6 @@ urlpatterns = [
|
|||||||
path("api/", include("bookings.urls")),
|
path("api/", include("bookings.urls")),
|
||||||
path("api/", include("checkins.urls")),
|
path("api/", include("checkins.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|||||||
@@ -7,11 +7,36 @@ from .models import Performance, Show, Venue
|
|||||||
|
|
||||||
@admin.register(Show)
|
@admin.register(Show)
|
||||||
class ShowAdmin(admin.ModelAdmin):
|
class ShowAdmin(admin.ModelAdmin):
|
||||||
list_display = ("title", "slug", "is_published", "created_at", "updated_at")
|
list_display = ("title", "slug", "is_published", "image_preview", "created_at", "updated_at")
|
||||||
list_filter = ("is_published",)
|
list_filter = ("is_published",)
|
||||||
search_fields = ("title", "slug", "summary", "description")
|
search_fields = ("title", "slug", "summary", "description")
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
readonly_fields = ("created_at", "updated_at")
|
readonly_fields = ("image_preview", "created_at", "updated_at")
|
||||||
|
fields = (
|
||||||
|
"title",
|
||||||
|
"slug",
|
||||||
|
"summary",
|
||||||
|
"description",
|
||||||
|
"uploaded_image",
|
||||||
|
"poster_image",
|
||||||
|
"image_preview",
|
||||||
|
"is_published",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.display(description="Preview")
|
||||||
|
def image_preview(self, obj):
|
||||||
|
if not getattr(obj, "pk", None):
|
||||||
|
return "-"
|
||||||
|
image_url = obj.image_url()
|
||||||
|
if not image_url:
|
||||||
|
return "No image"
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" alt="{}" style="max-width: 120px; border-radius: 6px;" />',
|
||||||
|
image_url,
|
||||||
|
obj.title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Venue)
|
@admin.register(Venue)
|
||||||
|
|||||||
17
backend/shows/migrations/0002_show_uploaded_image.py
Normal file
17
backend/shows/migrations/0002_show_uploaded_image.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.3 on 2026-04-29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("shows", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="show",
|
||||||
|
name="uploaded_image",
|
||||||
|
field=models.ImageField(blank=True, upload_to="shows/"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -16,6 +16,7 @@ class Show(TimeStampedModel):
|
|||||||
summary = models.TextField(blank=True)
|
summary = models.TextField(blank=True)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
poster_image = models.URLField(blank=True)
|
poster_image = models.URLField(blank=True)
|
||||||
|
uploaded_image = models.ImageField(upload_to="shows/", blank=True)
|
||||||
is_published = models.BooleanField(default=False, db_index=True)
|
is_published = models.BooleanField(default=False, db_index=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -28,6 +29,14 @@ class Show(TimeStampedModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
def image_url(self, request=None):
|
||||||
|
if self.uploaded_image:
|
||||||
|
image_url = self.uploaded_image.url
|
||||||
|
if request is not None:
|
||||||
|
return request.build_absolute_uri(image_url)
|
||||||
|
return image_url
|
||||||
|
return self.poster_image
|
||||||
|
|
||||||
|
|
||||||
class Venue(TimeStampedModel):
|
class Venue(TimeStampedModel):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class PublicShowListSerializer(serializers.Serializer):
|
class PublicShowImageMixin(serializers.Serializer):
|
||||||
|
image_url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_image_url(self, obj):
|
||||||
|
request = self.context.get("request")
|
||||||
|
return obj.image_url(request=request)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicShowListSerializer(PublicShowImageMixin, serializers.Serializer):
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
title = serializers.CharField()
|
title = serializers.CharField()
|
||||||
slug = serializers.SlugField()
|
slug = serializers.SlugField()
|
||||||
@@ -18,7 +26,7 @@ class PublicVenueDetailSerializer(PublicVenueSummarySerializer):
|
|||||||
address = serializers.CharField()
|
address = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
class PublicShowSummarySerializer(serializers.Serializer):
|
class PublicShowSummarySerializer(PublicShowImageMixin, serializers.Serializer):
|
||||||
title = serializers.CharField()
|
title = serializers.CharField()
|
||||||
slug = serializers.SlugField()
|
slug = serializers.SlugField()
|
||||||
summary = serializers.CharField()
|
summary = serializers.CharField()
|
||||||
|
|||||||
77
backend/shows/test_api.py
Normal file
77
backend/shows/test_api.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from shows.models import Performance, Show, Venue
|
||||||
|
|
||||||
|
|
||||||
|
SMALL_GIF = (
|
||||||
|
b"GIF89a\x01\x00\x01\x00\x80\x00\x00"
|
||||||
|
b"\x00\x00\x00\xff\xff\xff!\xf9\x04\x01"
|
||||||
|
b"\x00\x00\x00\x00,\x00\x00\x00\x00\x01"
|
||||||
|
b"\x00\x01\x00\x00\x02\x02D\x01\x00;"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowApiTests(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp_media = TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp_media.cleanup)
|
||||||
|
self.settings_override = override_settings(MEDIA_ROOT=self.temp_media.name)
|
||||||
|
self.settings_override.enable()
|
||||||
|
self.addCleanup(self.settings_override.disable)
|
||||||
|
|
||||||
|
self.show = Show.objects.create(
|
||||||
|
title="Open Stage",
|
||||||
|
slug="open-stage-media",
|
||||||
|
summary="A contemporary theatre performance.",
|
||||||
|
description="Full public show description.",
|
||||||
|
poster_image="https://cdn.example.com/open-stage-poster.jpg",
|
||||||
|
is_published=True,
|
||||||
|
)
|
||||||
|
self.external_only_show = Show.objects.create(
|
||||||
|
title="External Poster Stage",
|
||||||
|
slug="external-poster-stage",
|
||||||
|
summary="External image only.",
|
||||||
|
description="External image only.",
|
||||||
|
poster_image="https://cdn.example.com/external-only.jpg",
|
||||||
|
is_published=True,
|
||||||
|
)
|
||||||
|
self.venue = Venue.objects.create(
|
||||||
|
name="AzioneLab Theatre",
|
||||||
|
slug="azionelab-theatre-show-api",
|
||||||
|
address="Via Example 1",
|
||||||
|
city="Rome",
|
||||||
|
)
|
||||||
|
self.performance = Performance.objects.create(
|
||||||
|
show=self.show,
|
||||||
|
venue=self.venue,
|
||||||
|
starts_at=timezone.now() + timedelta(days=7),
|
||||||
|
room_capacity=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_show_list_prefers_uploaded_image_url_when_present(self):
|
||||||
|
self.show.uploaded_image.save(
|
||||||
|
"open-stage.gif",
|
||||||
|
SimpleUploadedFile("open-stage.gif", SMALL_GIF, content_type="image/gif"),
|
||||||
|
save=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("api-show-list"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
list_item = next(item for item in response.data["results"] if item["slug"] == self.show.slug)
|
||||||
|
self.assertTrue(list_item["image_url"].endswith("/media/shows/open-stage.gif"))
|
||||||
|
self.assertEqual(list_item["poster_image"], "https://cdn.example.com/open-stage-poster.jpg")
|
||||||
|
|
||||||
|
def test_show_detail_falls_back_to_existing_external_image_url(self):
|
||||||
|
response = self.client.get(reverse("api-show-detail", kwargs={"slug": self.external_only_show.slug}))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["image_url"], "https://cdn.example.com/external-only.jpg")
|
||||||
@@ -24,7 +24,7 @@ def public_performance_queryset():
|
|||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def show_list(request):
|
def show_list(request):
|
||||||
shows = Show.objects.filter(is_published=True).order_by("title")
|
shows = Show.objects.filter(is_published=True).order_by("title")
|
||||||
serializer = PublicShowListSerializer(shows, many=True)
|
serializer = PublicShowListSerializer(shows, many=True, context={"request": request})
|
||||||
return Response({"results": serializer.data})
|
return Response({"results": serializer.data})
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ def show_list(request):
|
|||||||
def show_detail(request, slug):
|
def show_detail(request, slug):
|
||||||
show = get_object_or_404(Show, slug=slug, is_published=True)
|
show = get_object_or_404(Show, slug=slug, is_published=True)
|
||||||
show.public_performances = public_performance_queryset().filter(show=show)
|
show.public_performances = public_performance_queryset().filter(show=show)
|
||||||
serializer = PublicShowDetailSerializer(show)
|
serializer = PublicShowDetailSerializer(show, context={"request": request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
@@ -54,12 +54,16 @@ def performance_list(request):
|
|||||||
)
|
)
|
||||||
performances = performances.filter(starts_at__gte=starts_from)
|
performances = performances.filter(starts_at__gte=starts_from)
|
||||||
|
|
||||||
serializer = PublicPerformanceListSerializer(performances.order_by("starts_at"), many=True)
|
serializer = PublicPerformanceListSerializer(
|
||||||
|
performances.order_by("starts_at"),
|
||||||
|
many=True,
|
||||||
|
context={"request": request},
|
||||||
|
)
|
||||||
return Response({"results": serializer.data})
|
return Response({"results": serializer.data})
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
def performance_detail(request, pk):
|
def performance_detail(request, pk):
|
||||||
performance = get_object_or_404(public_performance_queryset(), pk=pk)
|
performance = get_object_or_404(public_performance_queryset(), pk=pk)
|
||||||
serializer = PublicPerformanceDetailSerializer(performance)
|
serializer = PublicPerformanceDetailSerializer(performance, context={"request": request})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ The database should not be published to the host in production.
|
|||||||
Recommended volumes:
|
Recommended volumes:
|
||||||
|
|
||||||
- `postgres_data`: PostgreSQL data directory;
|
- `postgres_data`: PostgreSQL data directory;
|
||||||
- `media`: uploaded media and generated QR assets if stored on disk;
|
- `media`: uploaded show images and generated QR assets if stored on disk;
|
||||||
- `static`: collected Django static files if served by nginx from a shared volume.
|
- `static`: collected Django static files if served by nginx from a shared volume.
|
||||||
|
|
||||||
Generated QR codes may also be generated on demand instead of stored as files. If stored, they must not reveal personal data and access must remain controlled.
|
Generated QR codes may also be generated on demand instead of stored as files. If stored, they must not reveal personal data and access must remain controlled.
|
||||||
@@ -150,6 +150,7 @@ Required nginx configuration:
|
|||||||
- upstream backend service name and port;
|
- upstream backend service name and port;
|
||||||
- static frontend root;
|
- static frontend root;
|
||||||
- proxy rules for `/api/` and `/admin/`;
|
- proxy rules for `/api/` and `/admin/`;
|
||||||
|
- media root for `/media/` if uploaded assets are served by nginx from a shared volume;
|
||||||
- TLS certificate paths for production.
|
- TLS certificate paths for production.
|
||||||
|
|
||||||
Secrets must be provided through deployment-managed environment variables, Docker secrets, or another secret manager. Do not commit real secret values.
|
Secrets must be provided through deployment-managed environment variables, Docker secrets, or another secret manager. Do not commit real secret values.
|
||||||
@@ -214,6 +215,7 @@ Database rollback needs special care once migrations exist. Down migrations or b
|
|||||||
## Operational Notes
|
## Operational Notes
|
||||||
|
|
||||||
- Configure database backups before accepting real bookings.
|
- Configure database backups before accepting real bookings.
|
||||||
|
- Back up the shared media volume together with the database if staff uploads show images.
|
||||||
- Monitor backend errors, email delivery failures, and check-in failures.
|
- Monitor backend errors, email delivery failures, and check-in failures.
|
||||||
- Keep container images explicitly versioned; do not use `latest` tags.
|
- Keep container images explicitly versioned; do not use `latest` tags.
|
||||||
- Keep the system small until operational needs justify additional services.
|
- Keep the system small until operational needs justify additional services.
|
||||||
|
|||||||
@@ -45,9 +45,16 @@ import { ShowDetail, ShowPerformance, ShowsApiService } from '../services/shows-
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
} @else if (show()) {
|
} @else if (show()) {
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
|
<div class="hero-copy">
|
||||||
<p class="eyebrow">Show detail</p>
|
<p class="eyebrow">Show detail</p>
|
||||||
<h1>{{ show()!.title }}</h1>
|
<h1>{{ show()!.title }}</h1>
|
||||||
<p class="supporting">{{ show()!.description || show()!.summary }}</p>
|
<p class="supporting">{{ show()!.description || show()!.summary }}</p>
|
||||||
|
</div>
|
||||||
|
@if (show()!.image_url) {
|
||||||
|
<div class="hero-image-wrap">
|
||||||
|
<img class="hero-image" [src]="show()!.image_url" [alt]="show()!.title" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
@@ -115,9 +122,17 @@ import { ShowDetail, ShowPerformance, ShowsApiService } from '../services/shows-
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.2fr) minmax(280px, 420px);
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
color: var(--azionelab-accent);
|
color: var(--azionelab-accent);
|
||||||
@@ -138,6 +153,24 @@ import { ShowDetail, ShowPerformance, ShowsApiService } from '../services/shows-
|
|||||||
max-width: 64ch;
|
max-width: 64ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-image-wrap {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--azionelab-border);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(207, 71, 51, 0.16), rgba(15, 22, 36, 0.08)),
|
||||||
|
#f8f1ea;
|
||||||
|
box-shadow: var(--azionelab-shadow);
|
||||||
|
aspect-ratio: 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@@ -231,6 +264,10 @@ import { ShowDetail, ShowPerformance, ShowsApiService } from '../services/shows-
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
|
.page-header {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ import { ShowListItem, ShowsApiService } from '../services/shows-api.service';
|
|||||||
<div class="show-grid">
|
<div class="show-grid">
|
||||||
@for (show of shows(); track show.slug) {
|
@for (show of shows(); track show.slug) {
|
||||||
<mat-card class="show-card">
|
<mat-card class="show-card">
|
||||||
|
@if (show.image_url) {
|
||||||
|
<div class="show-image-wrap">
|
||||||
|
<img class="show-image" [src]="show.image_url" [alt]="show.title" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<mat-card-title>{{ show.title }}</mat-card-title>
|
<mat-card-title>{{ show.title }}</mat-card-title>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p>{{ show.summary }}</p>
|
<p>{{ show.summary }}</p>
|
||||||
@@ -157,6 +162,22 @@ import { ShowListItem, ShowsApiService } from '../services/shows-api.service';
|
|||||||
box-shadow: var(--azionelab-shadow);
|
box-shadow: var(--azionelab-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.show-image-wrap {
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid var(--azionelab-border);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(207, 71, 51, 0.14), rgba(15, 22, 36, 0.06)),
|
||||||
|
#f8f1ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.show-card mat-card-content {
|
.show-card mat-card-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type ShowListItem = {
|
|||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
|
image_url: string;
|
||||||
poster_image: string;
|
poster_image: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ services:
|
|||||||
- "${BACKEND_PORT:-8000}"
|
- "${BACKEND_PORT:-8000}"
|
||||||
volumes:
|
volumes:
|
||||||
- django_static:/app/staticfiles
|
- django_static:/app/staticfiles
|
||||||
|
- django_media:/app/media
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -70,6 +71,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./nginx/templates:/etc/nginx/templates:ro
|
- ./nginx/templates:/etc/nginx/templates:ro
|
||||||
- django_static:/var/www/static:ro
|
- django_static:/var/www/static:ro
|
||||||
|
- django_media:/var/www/media:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
@@ -80,6 +82,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
django_static:
|
django_static:
|
||||||
|
django_media:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
internal:
|
internal:
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /media/ {
|
location /media/ {
|
||||||
proxy_pass http://azionelab_backend;
|
alias /var/www/media/;
|
||||||
proxy_set_header Host $host;
|
access_log off;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
expires 1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
Reference in New Issue
Block a user