Skip to content
Merged
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,21 @@ docker run -d -p 6379:6379 --name redis redis:alpine
celery -A config beat -l info
```

```markdown
```bash
-A config - указывает где находится Celery-приложение
beat - запускает Celery Beat — компонент, который периодически отправляет задачи в очередь
-l info - уровень логирования (DEBUG, INFO, WARNING, ERROR)
```

## Команды для приложения

### в командной строке:
```bash
python manage.py create_default_tags # Заполнить БД стандартными тэгами
python manage.py assign_tags_to_books # Назначить тэги для книг
python manage.py parse_books # Запустить парсер книг
```

## 📄 Лицензия

Этот проект лицензирован под MIT License - см. файл [LICENSE](LICENSE) для деталей.
Expand Down
15 changes: 9 additions & 6 deletions apps/books/api/v1/filters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django_filters import CharFilter, DateFilter, FilterSet
from django_filters import CharFilter, DateFilter, FilterSet, NumberFilter

from ...models import Book

Expand All @@ -11,13 +11,16 @@ class BookFilter(FilterSet):
field_name="author__last_name",
lookup_expr="icontains",
)
publisher = CharFilter(
field_name="publisher__name",
lookup_expr="icontains",
publisher = NumberFilter(
field_name="publisher__id",
)
tag = NumberFilter(
field_name="tags__id",
lookup_expr="exact",
)
tag = CharFilter(
tag_name = CharFilter(
field_name="tags__name",
lookup_expr="iexact",
lookup_expr="icontains",
)
language = CharFilter(
lookup_expr="iexact",
Expand Down
9 changes: 4 additions & 5 deletions apps/books/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class Meta:
"publisher",
"published_at",
"cover_image",
"description",
"isbn_code",
"total_pages",
"language",
]


Expand All @@ -49,11 +53,6 @@ class BookDetailSerializer(BookSerializer):

class Meta(BookSerializer.Meta):
fields = BookSerializer.Meta.fields + [
"description",
"isbn_code",
"total_pages",
"cover_image",
"language",
"tags",
"comments",
]
Expand Down
17 changes: 16 additions & 1 deletion apps/books/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter
from rest_framework.permissions import AllowAny

from ...models import (
Author,
Expand All @@ -22,24 +24,36 @@
class PublisherViewSet(viewsets.ModelViewSet):
queryset = Publisher.objects.all()
serializer_class = PublisherSerializer
permission_classes = [AllowAny]


class AuthorViewSet(viewsets.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
permission_classes = [AllowAny]


class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [AllowAny]


class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.select_related("publisher").prefetch_related(
"author__books", "tags"
)
filter_backends = [DjangoFilterBackend]
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_class = BookFilter
ordering_fields = [
"title",
"published_at",
"created",
"publisher__name",
"author__last_name",
]
ordering = ["-created"]
permission_classes = [AllowAny]

def get_serializer_class(self):
if self.action == "retrieve":
Expand All @@ -50,6 +64,7 @@ def get_serializer_class(self):
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.select_related("user", "book")
serializer_class = CommentSerializer
permission_classes = [AllowAny]

def perform_create(self, serializer):
serializer.save(user=self.request.user)
67 changes: 67 additions & 0 deletions apps/books/management/commands/assign_tags_to_books.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.core.management.base import BaseCommand
from django.db import transaction

from logger.books.log import get_logger
from ...models import Book
from ...services.tag_matcher import find_matching_tags

logger = get_logger(__name__)


class Command(BaseCommand):
help = "сопоставляет существующие тэги с названиями всех книг в базе данных"

def add_arguments(self, parser):
parser.add_argument(
"--book-id",
type=int,
help="ID конкретной книги для обработки",
)

def handle(self, *args, **options):
book_id = options.get("book_id")

if book_id:
try:
book = Book.objects.get(id=book_id)
self.assign_tags_to_book(book)
logger.success(f"tags assigned successfully for book id {book_id}")
except Book.DoesNotExist:
logger.error(f"book with id {book_id} not found")
else:
self.assign_tags_to_all_books()

@transaction.atomic
def assign_tags_to_all_books(self):
"""Назначает тэги всем книгам в базе данных"""
books = Book.objects.all()
total_books = books.count()

logger.info(f"start matching tags for {total_books} books")

updated_count = 0
for book in books:
if self.assign_tags_to_book(book):
updated_count += 1

logger.success(
f"processed {total_books} books, updated tags for {updated_count} books"
)

def assign_tags_to_book(self, book):
"""
Назначает тэги конкретной книге
"""
matching_tags = find_matching_tags(book.title)

current_tags = set(book.tags.all())
new_tags = set(matching_tags)

if current_tags == new_tags:
logger.debug(f'no tag changes for book "{book.title}"')
return False

book.tags.set(matching_tags)
tag_names = [tag.name for tag in matching_tags]
logger.info(f'updated tags for book "{book.title}": {tag_names}')
return True
46 changes: 46 additions & 0 deletions apps/books/management/commands/create_default_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.core.management.base import BaseCommand

from logger.books.log import get_logger
from ...models import Tag


logger = get_logger(__name__)


class Command(BaseCommand):
help = "Создает предопределенные тэги для книг"

def handle(self, *args, **options):
default_tags = [
# Основные языки программирования
{"name": "Python", "slug": "python", "color": "#3776ab"},
{"name": "JavaScript", "slug": "javascript", "color": "#f7df1e"},
{"name": "Java", "slug": "java", "color": "#ed8b00"},
# Темы машинного обучения
{
"name": "Машинное обучение",
"slug": "machine-learning",
"color": "#ff6b6b",
},
{"name": "Искусственный интеллект", "slug": "ai", "color": "#feca57"},
{"name": "Data Science", "slug": "data-science", "color": "#96ceb4"},
{"name": "Нейронные сети", "slug": "neural-networks", "color": "#a55eea"},
# Разработка
{"name": "Алгоритмы", "slug": "algorithms", "color": "#4ecdc4"},
{"name": "Веб-разработка", "slug": "web-development", "color": "#45b7d1"},
{"name": "Базы данных", "slug": "databases", "color": "#ff9ff3"},
{"name": "DevOps", "slug": "devops", "color": "#54a0ff"},
{"name": "Тестирование", "slug": "testing", "color": "#5f27cd"},
{"name": "Архитектура", "slug": "architecture", "color": "#00d2d3"},
]
created_count = 0
for tag_data in default_tags:
tag, created = Tag.objects.get_or_create(
name=tag_data["name"], defaults=tag_data
)
if created:
created_count += 1
logger.success(f"{tag.name} tag created")
else:
logger.info(f"{tag.name} already exist!")
logger.success(f"Successfully created {created_count} new tags")
15 changes: 12 additions & 3 deletions apps/books/management/commands/parse_books.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,23 @@ async def scrape_book(self, url: str):
return None

parser = BookParser(html)
# Extract parameters first
params = parser.extract_all_params()

book_data = {
"url": url,
"book_title": parser.extract_book_name().get("book_title", ""),
"author": parser.extract_authors(),
"price": parser.extract_price(),
"details": parser.extract_all_params(),
"description": parser.extract_description().get("description", ""),
"cover": parser.extract_cover_image(),
"cover": {
"cover_image": parser.extract_cover_image().get("cover_image", "")
},
"details": {
"ISBN": params.get("ISBN", ""),
"Год": params.get("Год", ""),
"Страниц": int(params.get("Страниц", "0")) or 0,
},
"price": parser.extract_price(),
}
logger.debug(f"parsed book data for: {book_data['book_title']}")
return book_data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 5.1.7 on 2025-09-19 08:25

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("books", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="book",
name="electronic_price",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
verbose_name="Цена электронной версии",
),
),
migrations.AddField(
model_name="book",
name="price",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=10,
null=True,
verbose_name="Цена",
),
),
migrations.AddField(
model_name="book",
name="url",
field=models.URLField(blank=True, max_length=255, verbose_name="URL книги"),
),
migrations.AlterField(
model_name="publisher",
name="website",
field=models.URLField(
blank=True, max_length=255, verbose_name="Сайт издательства"
),
),
]
21 changes: 20 additions & 1 deletion apps/books/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Publisher(models.Model):
max_length=255,
)
website = models.URLField(
"Сайт издательства",
"Сайт издательства",
max_length=255,
blank=True,
)
Expand Down Expand Up @@ -97,6 +97,25 @@ class Book(TimeStampedModel):
"Язык",
max_length=50,
)
url = models.URLField(
"URL книги",
max_length=255,
blank=True,
)
price = models.DecimalField(
"Цена",
max_digits=10,
decimal_places=2,
null=True,
blank=True,
)
electronic_price = models.DecimalField(
"Цена электронной версии",
max_digits=10,
decimal_places=2,
null=True,
blank=True,
)

author = models.ManyToManyField(
Author,
Expand Down
Loading