diff --git a/bookify/bookify/settings.py b/bookify/bookify/settings.py
index 56b0c94..2a104bf 100644
--- a/bookify/bookify/settings.py
+++ b/bookify/bookify/settings.py
@@ -30,6 +30,9 @@ ALLOWED_HOSTS = []
# Application definition
+LOGIN_REDIRECT_URL = "/"
+LOGOUT_REDIRECT_URL = "/"
+
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
diff --git a/bookify/bookify/urls.py b/bookify/bookify/urls.py
index 1fc22eb..22da453 100644
--- a/bookify/bookify/urls.py
+++ b/bookify/bookify/urls.py
@@ -1,7 +1,19 @@
from django.contrib import admin
+from django.contrib.auth import views as auth_views
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("books.urls", namespace="books")),
+ # Встроенные Django view для входа-выхода (можем заменить на свои шаблоны)
+ path(
+ "login/",
+ auth_views.LoginView.as_view(template_name="accounts/login.html"),
+ name="login",
+ ),
+ path(
+ "logout/",
+ auth_views.LogoutView.as_view(template_name="accounts/logout.html"),
+ name="logout",
+ ),
]
diff --git a/bookify/books/admin.py b/bookify/books/admin.py
index b5d9935..b2a2e01 100644
--- a/bookify/books/admin.py
+++ b/bookify/books/admin.py
@@ -17,5 +17,5 @@ class BookAdmin(admin.ModelAdmin):
@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
- list_display = ("id", "book", "user_name", "rating", "created_at")
+ list_display = ("id", "book", "user", "rating", "created_at")
list_filter = ("book", "rating")
diff --git a/bookify/books/forms.py b/bookify/books/forms.py
index 5dd791c..4bee3c2 100644
--- a/bookify/books/forms.py
+++ b/bookify/books/forms.py
@@ -1,8 +1,17 @@
+# books/forms.py
from django import forms
+from django.contrib.auth.forms import UserCreationForm
+from django.contrib.auth.models import User
from .models import Book, Genre, Review
+class CustomUserCreationForm(UserCreationForm):
+ class Meta:
+ model = User
+ fields = ["username", "email"]
+
+
class BookForm(forms.ModelForm):
class Meta:
model = Book
@@ -15,7 +24,7 @@ class BookForm(forms.ModelForm):
class ReviewForm(forms.ModelForm):
class Meta:
model = Review
- fields = ["user_name", "rating", "text"]
+ fields = ["rating", "text"]
widgets = {
"rating": forms.NumberInput(attrs={"min": 1, "max": 5}),
}
diff --git a/bookify/books/models.py b/bookify/books/models.py
index 95e935f..22be8a9 100644
--- a/bookify/books/models.py
+++ b/bookify/books/models.py
@@ -1,5 +1,6 @@
from django.contrib.auth.models import User
from django.db import models
+from django.db.models import Avg # для подсчёта среднего рейтинга
class Genre(models.Model):
@@ -14,19 +15,33 @@ class Book(models.Model):
author = models.CharField(max_length=100)
description = models.TextField()
genres = models.ManyToManyField(Genre, related_name="books")
- cover_image = models.URLField(blank=True, null=True) # Можно хранить URL обложки
+ cover_image = models.URLField(blank=True, null=True)
+ created_by = models.ForeignKey(
+ User,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="created_books",
+ ) # пользователь, создавший книгу
def __str__(self):
return self.title
+ @property
+ def average_rating(self):
+ """Среднее значение рейтинга по всем отзывам"""
+ avg_rating = self.reviews.aggregate(Avg("rating"))["rating__avg"]
+ if avg_rating is not None:
+ return round(avg_rating, 1) # округлим до 1 знака после запятой
+ return 0
+
class Review(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="reviews")
- # Если планируется использовать Django-аутентификацию, можно связать с User
- user_name = models.CharField(max_length=100)
- rating = models.PositiveIntegerField(default=1) # от 1 до 5, например
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="reviews")
+ rating = models.PositiveIntegerField(default=1) # от 1 до 5
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
- return f"Отзыв от {self.user_name} на книгу «{self.book}»"
+ return f"Отзыв от {self.user.username} на «{self.book}»"
diff --git a/bookify/books/templates/accounts/login.html b/bookify/books/templates/accounts/login.html
new file mode 100644
index 0000000..77a3206
--- /dev/null
+++ b/bookify/books/templates/accounts/login.html
@@ -0,0 +1,16 @@
+{% load static %}
+
+
+
+
+ Вход
+
+
+Вход
+
+
+
diff --git a/bookify/books/templates/accounts/logout.html b/bookify/books/templates/accounts/logout.html
new file mode 100644
index 0000000..3df748f
--- /dev/null
+++ b/bookify/books/templates/accounts/logout.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Выход
+
+
+Вы вышли из аккаунта
+На главную
+
+
diff --git a/bookify/books/templates/accounts/register.html b/bookify/books/templates/accounts/register.html
new file mode 100644
index 0000000..1089110
--- /dev/null
+++ b/bookify/books/templates/accounts/register.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Регистрация
+
+
+Регистрация
+
+
+
diff --git a/bookify/books/templates/books/add_genre.html b/bookify/books/templates/books/add_genre.html
new file mode 100644
index 0000000..3bc8978
--- /dev/null
+++ b/bookify/books/templates/books/add_genre.html
@@ -0,0 +1,9 @@
+{% extends 'books/base.html' %}
+{% block content %}
+Добавить жанр
+
+{% endblock %}
diff --git a/bookify/books/templates/books/base.html b/bookify/books/templates/books/base.html
index 92e008d..f00b57f 100644
--- a/bookify/books/templates/books/base.html
+++ b/bookify/books/templates/books/base.html
@@ -4,14 +4,22 @@
- Приложение для книг
+ Bookify
diff --git a/bookify/books/templates/books/book_detail.html b/bookify/books/templates/books/book_detail.html
index fcca495..8ee1065 100644
--- a/bookify/books/templates/books/book_detail.html
+++ b/bookify/books/templates/books/book_detail.html
@@ -1,4 +1,3 @@
-
{% extends 'books/base.html' %}
{% block content %}
@@ -8,23 +7,26 @@
{% if book.cover_image %}
{% endif %}
-
-
Жанры:
+
Жанры:
{% for g in book.genres.all %}
- {{ g.name }} {% if not forloop.last %}, {% endif %}
+ {{ g.name }}
+ {% if not forloop.last %}, {% endif %}
{% endfor %}
+
Средний рейтинг: {{ book.average_rating }}
-
- Удалить книгу
-
+ {% if user.is_authenticated and user == book.created_by %}
+
+ Удалить книгу
+
+ {% endif %}
Отзывы
{% for review in reviews %}
-
{{ review.user_name }} ({{ review.rating }}/5)
+
{{ review.user.username }} ({{ review.rating }}/5)
{{ review.text }}
@@ -33,12 +35,16 @@
{% endfor %}
-
-
Добавить отзыв
-
-
+{% if user.is_authenticated %}
+
+
Добавить отзыв
+
+
+{% else %}
+ Для добавления отзывов войдите или зарегистрируйтесь .
+{% endif %}
{% endblock %}
diff --git a/bookify/books/templates/books/book_list.html b/bookify/books/templates/books/book_list.html
index 5cf3118..a7b77f5 100644
--- a/bookify/books/templates/books/book_list.html
+++ b/bookify/books/templates/books/book_list.html
@@ -1,17 +1,19 @@
-
{% extends 'books/base.html' %}
-{% load static %}
{% block content %}
Список книг
{% for book in books %}
-
+
+ {{ book.title }}
+ ({{ book.average_rating }})
+
Автор: {{ book.author }}
{% if book.genres.all %}
Жанры:
{% for g in book.genres.all %}
- {{ g.name }} {% if not forloop.last %}, {% endif %}
+ {{ g.name }}
+ {% if not forloop.last %}, {% endif %}
{% endfor %}
{% endif %}
diff --git a/bookify/books/templates/books/edit_genre.html b/bookify/books/templates/books/edit_genre.html
new file mode 100644
index 0000000..2a2fc96
--- /dev/null
+++ b/bookify/books/templates/books/edit_genre.html
@@ -0,0 +1,9 @@
+{% extends 'books/base.html' %}
+{% block content %}
+
Редактировать жанр
+
+{% endblock %}
diff --git a/bookify/books/templates/books/genre_list.html b/bookify/books/templates/books/genre_list.html
new file mode 100644
index 0000000..5321132
--- /dev/null
+++ b/bookify/books/templates/books/genre_list.html
@@ -0,0 +1,19 @@
+{% extends 'books/base.html' %}
+{% block content %}
+
Список жанров
+
+ {% for genre in genres %}
+
+ {{ genre.name }}
+
+ {% if user.is_authenticated %}
+ Редактировать
+ Удалить
+ {% endif %}
+
+ {% endfor %}
+
+{% if user.is_authenticated %}
+
Добавить жанр
+{% endif %}
+{% endblock %}
diff --git a/bookify/books/urls.py b/bookify/books/urls.py
index 40a0bc5..95136b8 100644
--- a/bookify/books/urls.py
+++ b/bookify/books/urls.py
@@ -1,3 +1,4 @@
+# books/urls.py
from django.urls import path
from . import views
@@ -10,9 +11,14 @@ urlpatterns = [
path("book/add/", views.add_book, name="add_book"),
path("book/delete/
/", views.delete_book, name="delete_book"),
path("book//add_review/", views.add_review, name="add_review"),
+ path("genres/", views.genre_list, name="genre_list"),
+ path("genres/add/", views.add_genre, name="add_genre"),
+ path("genres/edit//", views.edit_genre, name="edit_genre"),
+ path("genres/delete//", views.delete_genre, name="delete_genre"),
path(
"genres//",
views.genre_recommendations,
name="genre_recommendations",
),
+ path("register/", views.register, name="register"),
]
diff --git a/bookify/books/views.py b/bookify/books/views.py
index c309554..64a1b80 100644
--- a/bookify/books/views.py
+++ b/bookify/books/views.py
@@ -1,9 +1,26 @@
+from django.contrib.auth import login
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.forms import AuthenticationForm
+from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect, render
-from .forms import BookForm, ReviewForm
+from .forms import BookForm, CustomUserCreationForm, GenreForm, ReviewForm
from .models import Book, Genre, Review
+def register(request):
+ """Регистрация нового пользователя."""
+ if request.method == "POST":
+ form = CustomUserCreationForm(request.POST)
+ if form.is_valid():
+ user = form.save()
+ login(request, user) # сразу авторизуем после регистрации
+ return redirect("books:book_list")
+ else:
+ form = CustomUserCreationForm()
+ return render(request, "accounts/register.html", {"form": form})
+
+
def book_list(request):
"""Главная страница со списком всех книг."""
books = Book.objects.all()
@@ -22,33 +39,44 @@ def book_detail(request, pk):
)
+@login_required
def add_book(request):
- """Добавление новой книги."""
+ """Добавление новой книги (только авторизованный пользователь)."""
if request.method == "POST":
form = BookForm(request.POST)
if form.is_valid():
- form.save()
+ book = form.save(commit=False)
+ book.created_by = request.user
+ book.save()
+ form.save_m2m() # сохраняем многие-ко-многим
return redirect("books:book_list")
else:
form = BookForm()
return render(request, "books/add_book.html", {"form": form})
+@login_required
def delete_book(request, pk):
- """Удаление книги."""
+ """Удаление книги (только если user == created_by)."""
book = get_object_or_404(Book, pk=pk)
+ if book.created_by != request.user:
+ # Если не совпадает, то выбрасываем ошибку 403
+ raise PermissionDenied("Вы не можете удалять чужие книги.")
+
book.delete()
return redirect("books:book_list")
+@login_required
def add_review(request, pk):
- """Добавление отзыва к книге."""
+ """Добавление отзыва к книге (только авторизованный пользователь)."""
book = get_object_or_404(Book, pk=pk)
if request.method == "POST":
form = ReviewForm(request.POST)
if form.is_valid():
review = form.save(commit=False)
review.book = book
+ review.user = request.user
review.save()
return redirect("books:book_detail", pk=pk)
@@ -61,3 +89,44 @@ def genre_recommendations(request, genre_name):
return render(
request, "books/genre_recommendations.html", {"genre": genre, "books": books}
)
+
+
+def genre_list(request):
+ """Страница со всеми жанрами."""
+ genres = Genre.objects.all()
+ return render(request, "books/genre_list.html", {"genres": genres})
+
+
+@login_required
+def add_genre(request):
+ """Добавить новый жанр."""
+ if request.method == "POST":
+ form = GenreForm(request.POST)
+ if form.is_valid():
+ form.save()
+ return redirect("books:genre_list")
+ else:
+ form = GenreForm()
+ return render(request, "books/add_genre.html", {"form": form})
+
+
+@login_required
+def edit_genre(request, pk):
+ """Редактировать жанр."""
+ genre = get_object_or_404(Genre, pk=pk)
+ if request.method == "POST":
+ form = GenreForm(request.POST, instance=genre)
+ if form.is_valid():
+ form.save()
+ return redirect("books:genre_list")
+ else:
+ form = GenreForm(instance=genre)
+ return render(request, "books/edit_genre.html", {"form": form})
+
+
+@login_required
+def delete_genre(request, pk):
+ """Удалить жанр."""
+ genre = get_object_or_404(Genre, pk=pk)
+ genre.delete()
+ return redirect("books:genre_list")