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 %} + + + + + Вход + + +

Вход

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + 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 @@ + + + + + Регистрация + + +

Регистрация

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + 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 %} +

Добавить жанр

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% 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

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 %}
-
-

Добавить отзыв

-
- {% csrf_token %} - {{ review_form.as_p }} - -
-
+{% if user.is_authenticated %} +
+

Добавить отзыв

+
+ {% csrf_token %} + {{ review_form.as_p }} + +
+
+{% 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.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 %} +

Редактировать жанр

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% 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 %} +

Список жанров

+ +{% 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")