Жанры, пользователи
This commit is contained in:
@@ -30,6 +30,9 @@ ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}),
|
||||
}
|
||||
|
||||
@@ -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}»"
|
||||
|
||||
16
bookify/books/templates/accounts/login.html
Normal file
16
bookify/books/templates/accounts/login.html
Normal 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>
|
||||
11
bookify/books/templates/accounts/logout.html
Normal file
11
bookify/books/templates/accounts/logout.html
Normal 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>
|
||||
15
bookify/books/templates/accounts/register.html
Normal file
15
bookify/books/templates/accounts/register.html
Normal 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>
|
||||
9
bookify/books/templates/books/add_genre.html
Normal file
9
bookify/books/templates/books/add_genre.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
9
bookify/books/templates/books/edit_genre.html
Normal file
9
bookify/books/templates/books/edit_genre.html
Normal 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 %}
|
||||
19
bookify/books/templates/books/genre_list.html
Normal file
19
bookify/books/templates/books/genre_list.html
Normal 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 %}
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user