From 6d364d00c1da4be1fd806600f366f197f54bee3c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 15:12:47 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20SECRET=5FKEY=E3=81=A8DEBUG=E3=81=AE?= =?UTF-8?q?=E7=92=B0=E5=A2=83=E5=A4=89=E6=95=B0=E5=8C=96=E3=81=AB=E3=82=88?= =?UTF-8?q?=E3=82=8B=E3=82=BB=E3=82=AD=E3=83=A5=E3=83=AA=E3=83=86=E3=82=A3?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01F3cPFEgKvjkca2LAyWsjj2 --- .env.example | 4 ++++ config/settings.py | 18 +++++++++++++++--- requirements.txt | 1 + 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a6b697f --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Django settings +SECRET_KEY=change-me-to-a-real-secret-key +DEBUG=False +ALLOWED_HOSTS=localhost,127.0.0.1 diff --git a/config/settings.py b/config/settings.py index 60ee940..54fab09 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,12 +1,24 @@ +import os from pathlib import Path +from dotenv import load_dotenv + BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = "django-insecure-xzy_l8_hfjiq&n0y*hc5$4@+q7sg@i#xym_d$t65(m)d4rp2ty" +load_dotenv(BASE_DIR / ".env") + +SECRET_KEY = os.environ.get( + "SECRET_KEY", + "django-insecure-dev-only-fallback-do-not-use-in-production", +) -DEBUG = True +DEBUG = os.environ.get("DEBUG", "False").lower() in ("true", "1", "yes") -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [ + h.strip() + for h in os.environ.get("ALLOWED_HOSTS", "").split(",") + if h.strip() +] INSTALLED_APPS = [ "django.contrib.admin", diff --git a/requirements.txt b/requirements.txt index 22d4a11..8fb26ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Django>=4.2,<5.0 Pillow>=10.0 +python-dotenv>=1.0 From c8bdadcacdc71104ffb9ad25abdfbfe8d9aed176 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 15:13:30 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20Django=20LOGGING=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=A8=E3=83=93=E3=83=A5=E3=83=BC=E3=81=B8=E3=81=AE=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E5=87=BA=E5=8A=9B=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01F3cPFEgKvjkca2LAyWsjj2 --- accounts/models.py | 15 +++++++++++---- accounts/views.py | 10 ++++++++++ config/settings.py | 35 +++++++++++++++++++++++++++++++++++ spots/views.py | 11 +++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index aa3de1a..9873148 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,8 +1,12 @@ +import logging + from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver +logger = logging.getLogger("accounts") + class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") @@ -23,7 +27,10 @@ def get_display_name(self): @receiver(post_save, sender=User) def create_or_update_profile(sender, instance, created, **kwargs): - if created: - Profile.objects.create(user=instance) - else: - instance.profile.save() + try: + if created: + Profile.objects.create(user=instance) + else: + instance.profile.save() + except Exception: + logger.exception("Failed to create/update profile for user=%s", instance.username) diff --git a/accounts/views.py b/accounts/views.py index 152bd8a..d5f2dc3 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,9 +1,13 @@ +import logging + from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import login from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from .forms import SignUpForm, ProfileForm +logger = logging.getLogger("accounts") + def signup(request): if request.method == "POST": @@ -11,7 +15,10 @@ def signup(request): if form.is_valid(): user = form.save() login(request, user) + logger.info("New user signed up: %s", user.username) return redirect("spots:home") + else: + logger.warning("Signup form invalid: %s", form.errors) else: form = SignUpForm() return render(request, "accounts/signup.html", {"form": form}) @@ -24,7 +31,10 @@ def profile_edit(request): form = ProfileForm(request.POST, request.FILES, instance=profile) if form.is_valid(): form.save() + logger.info("Profile updated for user=%s", request.user.username) return redirect("accounts:user_profile", username=request.user.username) + else: + logger.warning("Profile edit form invalid for user=%s: %s", request.user.username, form.errors) else: form = ProfileForm(instance=profile) return render(request, "accounts/profile_edit.html", {"form": form}) diff --git a/config/settings.py b/config/settings.py index 54fab09..280fa2a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -90,3 +90,38 @@ LOGOUT_REDIRECT_URL = "spots:home" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOG_DIR = BASE_DIR / "logs" +LOG_DIR.mkdir(exist_ok=True) + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "[{asctime}] {levelname} {name} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + "file": { + "class": "logging.FileHandler", + "filename": LOG_DIR / "vibemap.log", + "formatter": "verbose", + }, + }, + "loggers": { + "spots": { + "handlers": ["console", "file"], + "level": "INFO", + }, + "accounts": { + "handlers": ["console", "file"], + "level": "INFO", + }, + }, +} diff --git a/spots/views.py b/spots/views.py index ce28d8f..cc5cd86 100644 --- a/spots/views.py +++ b/spots/views.py @@ -1,3 +1,5 @@ +import logging + from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.http import JsonResponse @@ -5,6 +7,8 @@ from .models import Spot, SpotImage, Like, Bookmark, Category from .forms import SpotForm, CommentForm +logger = logging.getLogger("spots") + def home(request): spots = Spot.objects.select_related("author", "category").prefetch_related("images")[:20] @@ -23,6 +27,7 @@ def spot_create(request): spot.save() for i, f in enumerate(files): SpotImage.objects.create(spot=spot, image=f, order=i) + logger.info("Spot created: id=%d title='%s' by user=%s", spot.pk, spot.title, request.user) return redirect("spots:spot_detail", pk=spot.pk) else: form = SpotForm() @@ -48,6 +53,7 @@ def spot_detail(request, pk): comment.user = request.user comment.spot = spot comment.save() + logger.info("Comment added on spot=%d by user=%s", pk, request.user) return redirect("spots:spot_detail", pk=pk) return render( @@ -74,6 +80,7 @@ def spot_edit(request, pk): spot.images.all().delete() for i, f in enumerate(files): SpotImage.objects.create(spot=spot, image=f, order=i) + logger.info("Spot edited: id=%d by user=%s", spot.pk, request.user) return redirect("spots:spot_detail", pk=spot.pk) else: form = SpotForm(instance=spot) @@ -84,6 +91,7 @@ def spot_edit(request, pk): def spot_delete(request, pk): spot = get_object_or_404(Spot, pk=pk, author=request.user) if request.method == "POST": + logger.info("Spot deleted: id=%d title='%s' by user=%s", spot.pk, spot.title, request.user) spot.delete() return redirect("spots:home") return render(request, "spots/spot_delete.html", {"spot": spot}) @@ -95,6 +103,7 @@ def spot_like(request, pk): like, created = Like.objects.get_or_create(user=request.user, spot=spot) if not created: like.delete() + logger.info("Spot %s: id=%d by user=%s", "liked" if created else "unliked", pk, request.user) if request.headers.get("X-Requested-With") == "XMLHttpRequest": return JsonResponse({"liked": created, "count": spot.like_count}) return redirect("spots:spot_detail", pk=pk) @@ -106,6 +115,7 @@ def spot_bookmark(request, pk): bookmark, created = Bookmark.objects.get_or_create(user=request.user, spot=spot) if not created: bookmark.delete() + logger.info("Spot %s: id=%d by user=%s", "bookmarked" if created else "unbookmarked", pk, request.user) if request.headers.get("X-Requested-With") == "XMLHttpRequest": return JsonResponse({"bookmarked": created}) return redirect("spots:spot_detail", pk=pk) @@ -125,6 +135,7 @@ def spot_search(request): if area: spots = spots.filter(area__icontains=area) + logger.info("Search: q='%s' category='%s' area='%s'", q, category_slug, area) categories = Category.objects.all() return render( request, From 58d76d3673feb2445d3056a666c1f2190cf6bd97 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 15:13:48 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E3=82=B9=E3=83=9D=E3=83=83=E3=83=88?= =?UTF-8?q?=E4=BD=9C=E6=88=90=E6=99=82=E3=81=AE=E3=83=88=E3=83=A9=E3=83=B3?= =?UTF-8?q?=E3=82=B6=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=A8=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://claude.ai/code/session_01F3cPFEgKvjkca2LAyWsjj2 --- spots/views.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/spots/views.py b/spots/views.py index cc5cd86..9829650 100644 --- a/spots/views.py +++ b/spots/views.py @@ -1,12 +1,17 @@ import logging -from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import JsonResponse +from django.db import transaction from django.db.models import Q +from django.http import JsonResponse +from django.shortcuts import render, redirect, get_object_or_404 from .models import Spot, SpotImage, Like, Bookmark, Category from .forms import SpotForm, CommentForm +ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} +MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB + logger = logging.getLogger("spots") @@ -16,17 +21,41 @@ def home(request): return render(request, "spots/home.html", {"spots": spots, "categories": categories}) +def _validate_image(f): + """Return an error message if the file is not a valid image, else None.""" + if f.content_type not in ALLOWED_IMAGE_TYPES: + return f"'{f.name}' は対応していないファイル形式です。JPEG/PNG/GIF/WebPのみ対応しています。" + if f.size > MAX_IMAGE_SIZE: + return f"'{f.name}' のファイルサイズが上限(10MB)を超えています。" + return None + + @login_required def spot_create(request): if request.method == "POST": form = SpotForm(request.POST) files = request.FILES.getlist("images") if form.is_valid() and files: - spot = form.save(commit=False) - spot.author = request.user - spot.save() - for i, f in enumerate(files): - SpotImage.objects.create(spot=spot, image=f, order=i) + # Validate all uploaded images before saving anything + for f in files: + error = _validate_image(f) + if error: + logger.warning("Image validation failed in spot_create: %s", error) + messages.error(request, error) + return render(request, "spots/spot_create.html", {"form": form}) + + try: + with transaction.atomic(): + spot = form.save(commit=False) + spot.author = request.user + spot.save() + for i, f in enumerate(files): + SpotImage.objects.create(spot=spot, image=f, order=i) + except Exception: + logger.exception("Failed to create spot for user=%s", request.user) + messages.error(request, "スポットの作成中にエラーが発生しました。もう一度お試しください。") + return render(request, "spots/spot_create.html", {"form": form}) + logger.info("Spot created: id=%d title='%s' by user=%s", spot.pk, spot.title, request.user) return redirect("spots:spot_detail", pk=spot.pk) else: