Skip to content

Commit 4fe2772

Browse files
committed
feat: add filters
2 parents 9c64134 + b20a585 commit 4fe2772

31 files changed

Lines changed: 822 additions & 305 deletions

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DEBUG=True
55
SECRET_KEY=supersecretkey
66
DJANGO_SECRET_KEY=supersecretkey
77
ALLOWED_HOSTS=localhost,127.0.0.1
8+
CORS_ALLOWED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
89

910
# =============================================
1011
# БАЗА ДАННЫХ (PostgreSQL)

.github/workflows/deploy_dev.yml

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,49 @@ jobs:
1010
runs-on: ubuntu-latest
1111

1212
steps:
13-
- name: Checkout code
14-
uses: actions/checkout@v4
15-
16-
- name: Deploy
17-
uses: appleboy/ssh-action@v1.0.0
18-
with:
19-
host: ${{ secrets.DEV_SERVER_HOST }}
20-
username: ${{ secrets.DEV_SSH_USERNAME }}
21-
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
22-
script: |
23-
cd ${{ secrets.DEV_PROJECT_PATH }}
24-
git pull origin develop
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Node
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
cache: 'npm'
21+
cache-dependency-path: frontend/package-lock.json
22+
23+
- name: Install deps (frontend)
24+
working-directory: frontend
25+
run: npm ci
26+
27+
- name: Build frontend
28+
working-directory: frontend
29+
env:
30+
VITE_API_BASE_URL: ${{ secrets.DEV_API_BASE_URL }}
31+
run: npm run build
32+
33+
- name: Deploy
34+
uses: appleboy/ssh-action@v1.0.0
35+
with:
36+
host: ${{ secrets.DEV_SERVER_HOST }}
37+
username: ${{ secrets.DEV_SSH_USERNAME }}
38+
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
39+
script: |
40+
cd ${{ secrets.DEV_PROJECT_PATH }}
41+
git pull origin develop
2542
26-
uv sync --no-dev
27-
uv run manage.py migrate
28-
29-
sudo systemctl restart pythonbooks
30-
31-
echo "Deployment successful!"
43+
uv sync --no-dev
44+
uv run manage.py migrate
45+
46+
sudo systemctl restart pythonbooks
47+
48+
echo "Deployment successful!"
49+
50+
- name: Upload frontend dist
51+
uses: appleboy/scp-action@v0.1.7
52+
with:
53+
host: ${{ secrets.DEV_SERVER_HOST }}
54+
username: ${{ secrets.DEV_SSH_USERNAME }}
55+
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}
56+
source: "frontend/dist/*"
57+
target: "${{ secrets.DEV_PROJECT_PATH }}"
58+
overwrite: true

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ dist/
4848
downloads/
4949
eggs/
5050
.eggs/
51-
lib/
52-
lib64/
51+
#lib/
52+
#lib64/
5353
parts/
5454
sdist/
5555
var/

apps/books/api/v1/serializers.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class Meta:
4040
"publisher",
4141
"published_at",
4242
"cover_image",
43+
"description",
44+
"isbn_code",
45+
"total_pages",
46+
"language",
4347
]
4448

4549

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

5054
class Meta(BookSerializer.Meta):
5155
fields = BookSerializer.Meta.fields + [
52-
"description",
53-
"isbn_code",
54-
"total_pages",
55-
"cover_image",
56-
"language",
5756
"tags",
5857
"comments",
5958
]

apps/books/api/v1/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ class BookViewSet(viewsets.ModelViewSet):
4545
)
4646
filter_backends = [DjangoFilterBackend, OrderingFilter]
4747
filterset_class = BookFilter
48-
ordering_fields = ["title", "published_at", "created", "publisher__name"]
48+
ordering_fields = [
49+
"title",
50+
"published_at",
51+
"created",
52+
"publisher__name",
53+
"author__last_name",
54+
]
4955
ordering = ["-created"]
5056
permission_classes = [AllowAny]
5157

apps/books/management/commands/parse_books.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,23 @@ async def scrape_book(self, url: str):
4545
return None
4646

4747
parser = BookParser(html)
48+
# Extract parameters first
49+
params = parser.extract_all_params()
50+
4851
book_data = {
4952
"url": url,
5053
"book_title": parser.extract_book_name().get("book_title", ""),
5154
"author": parser.extract_authors(),
52-
"price": parser.extract_price(),
53-
"details": parser.extract_all_params(),
5455
"description": parser.extract_description().get("description", ""),
55-
"cover": parser.extract_cover_image(),
56+
"cover": {
57+
"cover_image": parser.extract_cover_image().get("cover_image", "")
58+
},
59+
"details": {
60+
"ISBN": params.get("ISBN", ""),
61+
"Год": params.get("Год", ""),
62+
"Страниц": int(params.get("Страниц", "0")) or 0,
63+
},
64+
"price": parser.extract_price(),
5665
}
5766
logger.debug(f"parsed book data for: {book_data['book_title']}")
5867
return book_data
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Generated by Django 5.1.7 on 2025-09-19 08:25
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("books", "0001_initial"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="book",
14+
name="electronic_price",
15+
field=models.DecimalField(
16+
blank=True,
17+
decimal_places=2,
18+
max_digits=10,
19+
null=True,
20+
verbose_name="Цена электронной версии",
21+
),
22+
),
23+
migrations.AddField(
24+
model_name="book",
25+
name="price",
26+
field=models.DecimalField(
27+
blank=True,
28+
decimal_places=2,
29+
max_digits=10,
30+
null=True,
31+
verbose_name="Цена",
32+
),
33+
),
34+
migrations.AddField(
35+
model_name="book",
36+
name="url",
37+
field=models.URLField(blank=True, max_length=255, verbose_name="URL книги"),
38+
),
39+
migrations.AlterField(
40+
model_name="publisher",
41+
name="website",
42+
field=models.URLField(
43+
blank=True, max_length=255, verbose_name="Сайт издательства"
44+
),
45+
),
46+
]

apps/books/models.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Publisher(models.Model):
1111
max_length=255,
1212
)
1313
website = models.URLField(
14-
"Сайт издательства",
14+
"Сайт издательства",
1515
max_length=255,
1616
blank=True,
1717
)
@@ -97,6 +97,25 @@ class Book(TimeStampedModel):
9797
"Язык",
9898
max_length=50,
9999
)
100+
url = models.URLField(
101+
"URL книги",
102+
max_length=255,
103+
blank=True,
104+
)
105+
price = models.DecimalField(
106+
"Цена",
107+
max_digits=10,
108+
decimal_places=2,
109+
null=True,
110+
blank=True,
111+
)
112+
electronic_price = models.DecimalField(
113+
"Цена электронной версии",
114+
max_digits=10,
115+
decimal_places=2,
116+
null=True,
117+
blank=True,
118+
)
100119

101120
author = models.ManyToManyField(
102121
Author,

apps/books/scrapers/piter_publ/book_parser.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -77,46 +77,57 @@ def extract_cover_image(self):
7777
def extract_authors(self) -> List[Dict[str, str]]:
7878
try:
7979
authors = []
80-
author_blocks = self.soup.select("#tab-2 .autor-wrapper")
80+
author_blocks = self.soup.select(".autor-wrapper")
81+
if not author_blocks:
82+
author_blocks = self.soup.select("#tab-2 .autor-wrapper")
83+
84+
logger.info(f"found {len(author_blocks)} author blocks")
8185

8286
for block in author_blocks:
8387
name_tag = block.select_one("h2")
8488
if name_tag:
8589
full_name = name_tag.get_text(strip=True)
86-
parts = full_name.split()
87-
if len(parts) == 1:
88-
last_name = parts[0]
89-
first_name = ""
90-
elif len(parts) == 2:
91-
last_name, first_name = parts
92-
elif len(parts) >= 3:
93-
first_name = parts[1]
94-
last_name = " ".join([parts[0]] + parts[2:])
90+
else:
91+
name_element = block.find(["h1", "h3", "h4", "h5", "h6"])
92+
if name_element:
93+
full_name = name_element.get_text(strip=True)
9594
else:
96-
last_name = ""
97-
first_name = ""
98-
logger.warning(f"empty author name: {full_name}")
99-
100-
description_block = name_tag.parent
101-
bio_parts = []
102-
for bio in description_block.contents:
103-
if bio != name_tag and isinstance(bio, str):
104-
bio_parts.append(bio.strip())
105-
bio = " ".join(bio_parts).strip()
106-
107-
authors.append(
108-
{
109-
"first_name": first_name.strip("."),
110-
"last_name": last_name,
111-
"bio": bio,
112-
}
113-
)
114-
115-
logger.info(f"parsed {len(authors)} authors from tab-2")
95+
full_name = (
96+
block.get_text(strip=True).split("\n")[0]
97+
if block.get_text(strip=True)
98+
else ""
99+
)
100+
101+
if not full_name:
102+
continue
103+
104+
parts = full_name.split()
105+
if len(parts) >= 2:
106+
first_name = parts[0]
107+
last_name = " ".join(parts[1:])
108+
elif len(parts) == 1:
109+
first_name = ""
110+
last_name = parts[0]
111+
else:
112+
first_name = ""
113+
last_name = ""
114+
115+
full_text = block.get_text(separator=" ", strip=True)
116+
bio = full_text.replace(full_name, "", 1).strip()
117+
118+
authors.append(
119+
{
120+
"first_name": first_name.strip("."),
121+
"last_name": last_name,
122+
"bio": bio,
123+
}
124+
)
125+
126+
logger.info(f"parsed {len(authors)} authors")
116127
return authors
117128
except Exception as e:
118-
logger.error(f"failed to parse authors from tab-2: {str(e)}")
119-
logger.exception("tab-2 author parsing error details")
129+
logger.error(f"failed to parse authors: {str(e)}")
130+
logger.exception("author parsing error details")
120131
return []
121132

122133
def extract_author_bio(self) -> str:

apps/books/services/book_saver.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ def save_book(self, item: dict):
4141

4242
publisher = self.Publisher.get_or_create_publisher("Издательство Питер")
4343

44+
paper_price = None
45+
electronic_price = None
46+
if book_input.price and isinstance(book_input.price, dict):
47+
paper_price_str = book_input.price.get("price", "").replace("₽", "").strip()
48+
electronic_price_str = (
49+
book_input.price.get("electronic_price", "").replace("₽", "").strip()
50+
)
51+
if paper_price_str and paper_price_str.replace(".", "", 1).isdigit():
52+
paper_price = float(paper_price_str)
53+
if (
54+
electronic_price_str
55+
and electronic_price_str.replace(".", "", 1).isdigit()
56+
):
57+
electronic_price = float(electronic_price_str)
58+
4459
book = Book.objects.filter(isbn_code=isbn).first()
4560
if book:
4661
logger.info(f"updating book: {book_input.book_title} ({isbn})")
@@ -51,6 +66,9 @@ def save_book(self, item: dict):
5166
book.cover_image = book_input.cover.cover_image or ""
5267
book.language = "Русский"
5368
book.publisher = publisher
69+
book.url = book_input.url or ""
70+
book.price = paper_price
71+
book.electronic_price = electronic_price
5472
book.save()
5573
else:
5674
logger.info(f"creating new book: {book_input.book_title} ({isbn})")
@@ -63,6 +81,9 @@ def save_book(self, item: dict):
6381
cover_image=book_input.cover.cover_image or "",
6482
language="Русский",
6583
publisher=publisher,
84+
url=book_input.url or "",
85+
price=paper_price,
86+
electronic_price=electronic_price,
6687
)
6788

6889
authors = []

0 commit comments

Comments
 (0)