Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Django settings
SECRET_KEY=change-me-to-a-real-secret-key
DEBUG=False
ALLOWED_HOSTS=localhost,127.0.0.1
15 changes: 11 additions & 4 deletions accounts/models.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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)
10 changes: 10 additions & 0 deletions accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
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":
form = SignUpForm(request.POST)
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})
Expand All @@ -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})
Expand Down
53 changes: 50 additions & 3 deletions config/settings.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -78,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",
},
},
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Django>=4.2,<5.0
Pillow>=10.0
python-dotenv>=1.0
54 changes: 47 additions & 7 deletions spots/views.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,62 @@
from django.shortcuts import render, redirect, get_object_or_404
import logging

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")


def home(request):
spots = Spot.objects.select_related("author", "category").prefetch_related("images")[:20]
categories = Category.objects.all()
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:
form = SpotForm()
Expand All @@ -48,6 +82,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(
Expand All @@ -74,6 +109,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)
Expand All @@ -84,6 +120,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})
Expand All @@ -95,6 +132,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)
Expand All @@ -106,6 +144,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)
Expand All @@ -125,6 +164,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,
Expand Down