Refactor isbn api + allow updating with decitre in admin
This commit is contained in:
parent
c2e9b9e86b
commit
9d7b8a1964
4 changed files with 153 additions and 101 deletions
|
@ -3,7 +3,7 @@ from import_export import resources, fields
|
|||
from import_export.admin import ExportMixin
|
||||
from import_export.widgets import IntegerWidget, DecimalWidget
|
||||
|
||||
from manuels.models import Teacher, Book, Level, Editor, SuppliesRequirement, CommonSupply
|
||||
from manuels.models import Teacher, Book, Level, Editor, SuppliesRequirement, CommonSupply, ISBNError
|
||||
|
||||
|
||||
class TeacherResource(resources.ModelResource):
|
||||
|
@ -126,6 +126,24 @@ class BookAdmin(ExportMixin, admin.ModelAdmin):
|
|||
]
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
def update_with_decitre(self, request, queryset):
|
||||
for book in queryset:
|
||||
try:
|
||||
book.update_from_decitre()
|
||||
messages.success(
|
||||
request,
|
||||
f'Mise à jour réussie du livre "{book.title}" ({book.level.name} - {book.field})'
|
||||
)
|
||||
except ISBNError as e:
|
||||
messages.warning(
|
||||
request,
|
||||
f'Erreur lors de la mise à jour du livre "{book.title}" ({book.level.name} - {book.field}) : {e.data.get("error")}'
|
||||
)
|
||||
|
||||
update_with_decitre.short_description = 'Mettre à jour avec Decitre'
|
||||
|
||||
actions = [update_with_decitre]
|
||||
|
||||
|
||||
@admin.register(Editor)
|
||||
class EditorAdmin(admin.ModelAdmin):
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
import uuid as uuid
|
||||
|
||||
import bs4
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import CIEmailField
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -12,6 +14,15 @@ from django.db.models import Sum
|
|||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from manuels.utils import validate_isbn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ISBNError(Exception):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
class Meta:
|
||||
|
@ -246,6 +257,102 @@ class Book(BaseModel):
|
|||
from django.urls import reverse
|
||||
return reverse('edit_book', kwargs={'teacher_pk': str(self.teacher.pk), 'pk': str(self.pk)})
|
||||
|
||||
def update_from_decitre(self):
|
||||
decitre_data = self.fetch_from_decitre(self.isbn)
|
||||
self.title = decitre_data.get('title')
|
||||
self.authors = decitre_data.get('authors')
|
||||
self.price = decitre_data.get('price')
|
||||
self.publication_year = decitre_data.get('year')
|
||||
editor = decitre_data.get('editor')
|
||||
try:
|
||||
self.editor = Editor.objects.get(name__icontains=editor)
|
||||
except Editor.DoesNotExist:
|
||||
self.editor = Editor.objects.get(name__istartswith='autre')
|
||||
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
def fetch_from_decitre(isbn: str):
|
||||
isbn = isbn.strip().replace('-', '')
|
||||
|
||||
if not validate_isbn(isbn):
|
||||
raise ISBNError({
|
||||
'error': "L'ISBN saisi n'est pas valide."
|
||||
})
|
||||
|
||||
if len(isbn) == 10:
|
||||
raise ISBNError({
|
||||
'error': "La recherche sur Decitre ne fonctionne qu'avec un ISBN 13 (ou EAN)."
|
||||
})
|
||||
|
||||
try:
|
||||
res = requests.get(f'https://www.decitre.fr/livres/{isbn}.html', timeout=10)
|
||||
except requests.exceptions.Timeout as exc:
|
||||
raise ISBNError({
|
||||
'error': "Decitre n'a pas répondu dans les temps. Message : {}".format(str(exc))
|
||||
})
|
||||
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
message = (
|
||||
"Erreur lors de la recherche. Il se peut que le livre n'existe pas dans la base de connaissances "
|
||||
"de Decitre ou que vous ayez mal saisi l'ISBN. Vous pouvez toujours saisir "
|
||||
"les informations du livre à la main. Message : {}").format(str(exc))
|
||||
raise ISBNError({
|
||||
'error': message
|
||||
})
|
||||
|
||||
decitre_soup = bs4.BeautifulSoup(res.text, "html.parser")
|
||||
title = decitre_soup.select('h1.product-title')
|
||||
if title:
|
||||
title = title[0]
|
||||
if title.span:
|
||||
title.span.extract()
|
||||
title = title.get_text(strip=True)
|
||||
|
||||
authors = decitre_soup.select('.authors')
|
||||
if authors:
|
||||
authors = authors[0]
|
||||
authors = authors.get_text(strip=True)
|
||||
|
||||
price = decitre_soup.select('div.price span.final-price')
|
||||
if price:
|
||||
price = price[0]
|
||||
price = price.get_text().replace('€', '').replace(',', '.').strip()
|
||||
|
||||
year = None
|
||||
editor = None
|
||||
extra_info = decitre_soup.select('.informations-container')
|
||||
logger.debug('raw extra_info')
|
||||
logger.debug(extra_info)
|
||||
if not extra_info:
|
||||
logger.debug('#fiche-technique')
|
||||
extra_info = decitre_soup.select('#fiche-technique')
|
||||
logger.debug('raw extra_info fiche technique')
|
||||
logger.debug(extra_info)
|
||||
|
||||
if extra_info:
|
||||
extra_info = extra_info[0].get_text(strip=True)
|
||||
logger.debug('extra_info')
|
||||
logger.debug(extra_info)
|
||||
matches = re.search(
|
||||
r'Date de parution(?: :)?\d{2}/\d{2}/(?P<year>\d{4})Editeur(?: :)?(?P<editor>.+?)(?:ISBN|Collection)',
|
||||
extra_info)
|
||||
if matches:
|
||||
groups = matches.groupdict()
|
||||
year = groups.get('year')
|
||||
editor = groups.get('editor').strip()
|
||||
|
||||
return {
|
||||
'title': title,
|
||||
'authors': authors,
|
||||
'isbn': isbn,
|
||||
'price': float(price) if price else None,
|
||||
'year': year,
|
||||
'editor': editor,
|
||||
}
|
||||
|
||||
|
||||
class SuppliesRequirement(BaseModel):
|
||||
class Meta:
|
||||
|
|
21
manuels/utils.py
Normal file
21
manuels/utils.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
def validate_isbn(isbn):
|
||||
_sum = 0
|
||||
if len(isbn) == 10:
|
||||
for i, digit in enumerate(isbn):
|
||||
if digit == 'X':
|
||||
digit = 10
|
||||
else:
|
||||
digit = int(digit)
|
||||
_sum += digit * (i + 1)
|
||||
|
||||
return _sum % 11 == 0
|
||||
|
||||
elif len(isbn) == 13:
|
||||
for i, digit in enumerate(isbn):
|
||||
weight = 3 if i % 2 == 1 else 1
|
||||
digit = int(digit)
|
||||
_sum += digit * weight
|
||||
|
||||
return _sum % 10 == 0
|
||||
|
||||
return False
|
104
manuels/views.py
104
manuels/views.py
|
@ -11,7 +11,8 @@ from django.views.decorators.cache import cache_page
|
|||
from django.views.generic import CreateView, UpdateView, DeleteView, TemplateView
|
||||
|
||||
from manuels.forms import AddBookForm, AddSuppliesForm, EditBookForm, EditSuppliesForm
|
||||
from manuels.models import Teacher, Book, SuppliesRequirement, CommonSupply
|
||||
from manuels.models import Teacher, Book, SuppliesRequirement, CommonSupply, ISBNError
|
||||
from manuels.utils import validate_isbn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -245,105 +246,10 @@ class ConfirmTeacherView(BaseTeacherView, UpdateView):
|
|||
return response
|
||||
|
||||
|
||||
def validate_isbn(isbn):
|
||||
_sum = 0
|
||||
if len(isbn) == 10:
|
||||
for i, digit in enumerate(isbn):
|
||||
if digit == 'X':
|
||||
digit = 10
|
||||
else:
|
||||
digit = int(digit)
|
||||
_sum += digit * (i + 1)
|
||||
|
||||
return _sum % 11 == 0
|
||||
|
||||
elif len(isbn) == 13:
|
||||
for i, digit in enumerate(isbn):
|
||||
weight = 3 if i % 2 == 1 else 1
|
||||
digit = int(digit)
|
||||
_sum += digit * weight
|
||||
|
||||
return _sum % 10 == 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# We are able to cache the response because it's very unlikely that the details of a book will change through time
|
||||
@cache_page(2 * 60 * 60)
|
||||
def isbn_api(request, isbn):
|
||||
isbn = isbn.strip().replace('-', '')
|
||||
|
||||
if not validate_isbn(isbn):
|
||||
return JsonResponse({
|
||||
'error': "L'ISBN saisi n'est pas valide."
|
||||
})
|
||||
|
||||
if len(isbn) == 10:
|
||||
return JsonResponse({
|
||||
'error': "La recherche sur Decitre ne fonctionne qu'avec un ISBN 13 (ou EAN)."
|
||||
})
|
||||
|
||||
try:
|
||||
res = requests.get(f'https://www.decitre.fr/livres/{isbn}.html', timeout=10)
|
||||
except requests.exceptions.Timeout as exc:
|
||||
return JsonResponse({
|
||||
'error': "Decitre n'a pas répondu dans les temps. Message : {}".format(str(exc))
|
||||
})
|
||||
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
message = ("Erreur lors de la recherche. Il se peut que le livre n'existe pas dans la base de connaissances "
|
||||
"de Decitre ou que vous ayez mal saisi l'ISBN. Vous pouvez toujours saisir "
|
||||
"les informations du livre à la main. Message : {}").format(str(exc))
|
||||
return JsonResponse({
|
||||
'error': message
|
||||
})
|
||||
|
||||
decitre_soup = bs4.BeautifulSoup(res.text, "html.parser")
|
||||
title = decitre_soup.select('h1.product-title')
|
||||
if title:
|
||||
title = title[0]
|
||||
if title.span:
|
||||
title.span.extract()
|
||||
title = title.get_text(strip=True)
|
||||
|
||||
authors = decitre_soup.select('.authors')
|
||||
if authors:
|
||||
authors = authors[0]
|
||||
authors = authors.get_text(strip=True)
|
||||
|
||||
price = decitre_soup.select('div.price span.final-price')
|
||||
if price:
|
||||
price = price[0]
|
||||
price = price.get_text().replace('€', '').replace(',', '.').strip()
|
||||
|
||||
year = None
|
||||
editor = None
|
||||
extra_info = decitre_soup.select('.informations-container')
|
||||
logger.debug('raw extra_info')
|
||||
logger.debug(extra_info)
|
||||
if not extra_info:
|
||||
logger.debug('#fiche-technique')
|
||||
extra_info = decitre_soup.select('#fiche-technique')
|
||||
logger.debug('raw extra_info fiche technique')
|
||||
logger.debug(extra_info)
|
||||
|
||||
if extra_info:
|
||||
extra_info = extra_info[0].get_text(strip=True)
|
||||
logger.debug('extra_info')
|
||||
logger.debug(extra_info)
|
||||
matches = re.search(r'Date de parution(?: :)?\d{2}/\d{2}/(?P<year>\d{4})Editeur(?: :)?(?P<editor>.+?)(?:ISBN|Collection)', extra_info)
|
||||
if matches:
|
||||
groups = matches.groupdict()
|
||||
year = groups.get('year')
|
||||
editor = groups.get('editor').strip()
|
||||
|
||||
return JsonResponse({
|
||||
'title': title,
|
||||
'authors': authors,
|
||||
'isbn': isbn,
|
||||
'price': float(price) if price else None,
|
||||
'year': year,
|
||||
'editor': editor,
|
||||
})
|
||||
return JsonResponse(Book.fetch_from_decitre(isbn))
|
||||
except ISBNError as e:
|
||||
return JsonResponse(e.data)
|
||||
|
|
Loading…
Reference in a new issue