Жанры, пользователи

This commit is contained in:
2025-01-16 14:17:29 +03:00
parent 8d1b2c357e
commit ec6a075e41
16 changed files with 243 additions and 34 deletions

View File

@@ -30,6 +30,9 @@ ALLOWED_HOSTS = []
# Application definition
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",

View File

@@ -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",
),
]

View File

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

View File

@@ -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}),
}

View File

@@ -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}»"

View File

@@ -0,0 +1,16 @@
{% load static %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Вход</title>
</head>
<body>
<h1>Вход</h1>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Войти</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Выход</title>
</head>
<body>
<h1>Вы вышли из аккаунта</h1>
<a href="{% url 'books:book_list' %}">На главную</a>
</body>
</html>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Регистрация</title>
</head>
<body>
<h1>Регистрация</h1>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Зарегистрироваться</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,9 @@
{% extends 'books/base.html' %}
{% block content %}
<h2>Добавить жанр</h2>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Сохранить</button>
</form>
{% endblock %}

View File

@@ -4,14 +4,22 @@
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Приложение для книг</title>
<title>Bookify</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<header>
<h1><a href="{% url 'books:book_list' %}">Bookify</a></h1>
<nav>
<a href="{% url 'books:add_book' %}">Добавить книгу</a>
<a href="{% url 'books:book_list' %}">Список книг</a>
<a href="{% url 'books:genre_list' %}">Список жанров</a>
{% if user.is_authenticated %}
<a href="{% url 'books:add_book' %}">Добавить книгу</a>
<a href="{% url 'logout' %}">Выйти</a>
{% else %}
<a href="{% url 'login' %}">Войти</a>
<a href="{% url 'books:register' %}">Регистрация</a>
{% endif %}
</nav>
</header>

View File

@@ -1,4 +1,3 @@
<!-- books/templates/books/book_detail.html -->
{% extends 'books/base.html' %}
{% block content %}
<div class="book-detail">
@@ -8,23 +7,26 @@
{% if book.cover_image %}
<img src="{{ book.cover_image }}" alt="Обложка книги" style="max-width:200px;">
{% endif %}
<p><strong>Жанры:</strong>
{% for g in book.genres.all %}
<a href="{% url 'books:genre_recommendations' g.name %}">{{ g.name }}</a>{% if not forloop.last %}, {% endif %}
<a href="{% url 'books:genre_recommendations' g.name %}">{{ g.name }}</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
<p><strong>Средний рейтинг:</strong> {{ book.average_rating }}</p>
<p>
<a class="btn-delete" href="{% url 'books:delete_book' book.pk %}">Удалить книгу</a>
</p>
{% if user.is_authenticated and user == book.created_by %}
<p>
<a class="btn-delete" href="{% url 'books:delete_book' book.pk %}">Удалить книгу</a>
</p>
{% endif %}
</div>
<div class="reviews-section">
<h3>Отзывы</h3>
{% for review in reviews %}
<div class="review">
<p><strong>{{ review.user_name }}</strong> ({{ review.rating }}/5)</p>
<p><strong>{{ review.user.username }}</strong> ({{ review.rating }}/5)</p>
<p>{{ review.text }}</p>
<hr>
</div>
@@ -33,12 +35,16 @@
{% endfor %}
</div>
<div class="add-review">
<h3>Добавить отзыв</h3>
<form method="POST" action="{% url 'books:add_review' book.pk %}">
{% csrf_token %}
{{ review_form.as_p }}
<button type="submit">Отправить</button>
</form>
</div>
{% if user.is_authenticated %}
<div class="add-review">
<h3>Добавить отзыв</h3>
<form method="POST" action="{% url 'books:add_review' book.pk %}">
{% csrf_token %}
{{ review_form.as_p }}
<button type="submit">Отправить</button>
</form>
</div>
{% else %}
<p>Для добавления отзывов <a href="{% url 'login' %}">войдите</a> или <a href="{% url 'books:register' %}">зарегистрируйтесь</a>.</p>
{% endif %}
{% endblock %}

View File

@@ -1,17 +1,19 @@
<!-- books/templates/books/book_list.html -->
{% extends 'books/base.html' %}
{% load static %}
{% block content %}
<h2>Список книг</h2>
<div class="book-list">
{% for book in books %}
<div class="book-item">
<h3><a href="{% url 'books:book_detail' book.pk %}">{{ book.title }}</a></h3>
<h3>
<a href="{% url 'books:book_detail' book.pk %}">{{ book.title }}</a>
<small>({{ book.average_rating }})</small> <!-- Средний рейтинг -->
</h3>
<p>Автор: {{ book.author }}</p>
{% if book.genres.all %}
<p>Жанры:
{% for g in book.genres.all %}
<a href="{% url 'books:genre_recommendations' g.name %}">{{ g.name }}</a>{% if not forloop.last %}, {% endif %}
<a href="{% url 'books:genre_recommendations' g.name %}">{{ g.name }}</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}

View File

@@ -0,0 +1,9 @@
{% extends 'books/base.html' %}
{% block content %}
<h2>Редактировать жанр</h2>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Сохранить</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'books/base.html' %}
{% block content %}
<h2>Список жанров</h2>
<ul>
{% for genre in genres %}
<li>
{{ genre.name }}
<!-- ссылки на редактирование/удаление жанра -->
{% if user.is_authenticated %}
<a href="{% url 'books:edit_genre' genre.pk %}">Редактировать</a>
<a href="{% url 'books:delete_genre' genre.pk %}">Удалить</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% if user.is_authenticated %}
<a href="{% url 'books:add_genre' %}">Добавить жанр</a>
{% endif %}
{% endblock %}

View File

@@ -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/<int:pk>/", views.delete_book, name="delete_book"),
path("book/<int:pk>/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/<int:pk>/", views.edit_genre, name="edit_genre"),
path("genres/delete/<int:pk>/", views.delete_genre, name="delete_genre"),
path(
"genres/<str:genre_name>/",
views.genre_recommendations,
name="genre_recommendations",
),
path("register/", views.register, name="register"),
]

View File

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