Merge pull request #77

* added timestamp to comicstatus.

* added timestamp to comicstatus.
This commit is contained in:
2022-09-19 12:18:46 +01:00
committed by GitHub
parent 01e73cc9b3
commit 2647a0e31e
20 changed files with 463 additions and 304 deletions

View File

@@ -46,6 +46,7 @@ router.register(r'generate_thumbnail', rest.GenerateThumbnailViewSet, basename='
router.register(r'read', rest.ReadViewSet, basename='read')\ router.register(r'read', rest.ReadViewSet, basename='read')\
.register(r'image', rest.ImageViewSet, basename='image', parents_query_lookups=['selector']) .register(r'image', rest.ImageViewSet, basename='image', parents_query_lookups=['selector'])
router.register(r'recent', rest.RecentComicsView, basename="recent") router.register(r'recent', rest.RecentComicsView, basename="recent")
router.register(r'history', rest.HistoryViewSet, basename='history')
router.register(r'action', rest.ActionViewSet, basename='action') router.register(r'action', rest.ActionViewSet, basename='action')
router.register(r'account', rest.AccountViewSet, basename='account') router.register(r'account', rest.AccountViewSet, basename='account')
router.register(r'directory', rest.DirectoryViewSet, basename='directory') router.register(r'directory', rest.DirectoryViewSet, basename='directory')

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import admin from django.contrib import admin
from .models import Directory, ComicBook, ComicPage, ComicStatus, UserMisc from .models import Directory, ComicBook, ComicStatus, UserMisc
@admin.register(Directory) @admin.register(Directory)
@@ -26,12 +26,6 @@ class ComicBookAdmin(admin.ModelAdmin):
search_fields = ['file_name'] search_fields = ['file_name']
@admin.register(ComicPage)
class ComicPageAdmin(admin.ModelAdmin):
list_display = ('id', 'Comic', 'index', 'page_file_name', 'content_type')
raw_id_fields = ('Comic',)
@admin.register(ComicStatus) @admin.register(ComicStatus)
class ComicStatusAdmin(admin.ModelAdmin): class ComicStatusAdmin(admin.ModelAdmin):
list_display = ( list_display = (

View File

@@ -1,114 +1,38 @@
from pathlib import Path from typing import Optional, Union
from PIL import UnidentifiedImageError from django.contrib.auth import get_user_model
from django.conf import settings from django.contrib.auth.models import User
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand, CommandParser
from loguru import logger from loguru import logger
from comic.models import ComicBook, Directory from comic.models import ComicBook, Directory
from comic.processing import generate_directory
class Command(BaseCommand): class Command(BaseCommand):
help = "Scan directories to Update Comic DB" help = "Scan directories to Update Comic DB"
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self.OUTPUT = False self.OUTPUT = False
self.VERIFY_PAGES = False
def add_arguments(self, parser): def add_arguments(self, parser: CommandParser) -> None:
parser.add_argument( parser.add_argument(
'--out', '--out',
action='store_true', action='store_true',
help='Output to console', help='Output to console',
) )
parser.add_argument(
'--verify_pages',
action='store_true',
help='Output to console',
)
def handle(self, *args, **options): def handle(self, *args, **options) -> None:
self.VERIFY_PAGES = options.get('verify_pages', False)
self.OUTPUT = options.get('out', False) self.OUTPUT = options.get('out', False)
self.scan_directory() self.scan_directory()
def scan_directory(self, directory=False): def scan_directory(self, user: Optional[User] = None, directory: Optional[Directory] = None) -> None:
if not user:
""" user_model = get_user_model()
user = user_model.objects.first()
:type directory: Directory for item in generate_directory(user, directory):
""" item: Union[Directory, ComicBook]
if not directory: if item.type == 'Directory':
comic_dir = settings.COMIC_BOOK_VOLUME logger.info(item)
else: self.scan_directory(user, item)
comic_dir = Path(settings.COMIC_BOOK_VOLUME, directory.path)
if directory:
books = ComicBook.objects.filter(directory=directory)
else:
books = ComicBook.objects.filter(directory__isnull=True)
for book in books:
Path(comic_dir, book.file_name).is_file()
if not Path(comic_dir, book.file_name).is_file():
book.delete()
for file in sorted(comic_dir.glob('*')):
if file.is_dir():
if self.OUTPUT:
logger.info(f"Scanning Directory {file}")
try:
if directory:
next_directory, created = Directory.objects.get_or_create(name=file.name, parent=directory)
else:
next_directory, created = Directory.objects.get_or_create(name=file.name, parent__isnull=True)
except Directory.MultipleObjectsReturned:
if directory:
next_directories = Directory.objects.filter(name=file.name, parent=directory)
else:
next_directories = Directory.objects.filter(name=file.name, parent__isnull=True)
next_directories = next_directories.order_by('id')
next_directory = next_directories.first()
next_directories.exclude(id=next_directory.id).delete()
logger.error(f'Duplicate Directory {file}')
created = False
if created:
next_directory.save()
self.scan_directory(next_directory)
else:
if file.suffix.lower() not in settings.SUPPORTED_FILES:
continue
if self.OUTPUT:
logger.info(f"Scanning File {file}")
try:
if directory:
try:
book = ComicBook.objects.get(file_name=file.name, directory=directory)
except ComicBook.MultipleObjectsReturned:
logger.error(f'Duplicate Comic {file}')
books = ComicBook.objects.filter(file_name=file.name, directory=directory).order_by('id')
book = books.first()
extra_books = books.exclude(id=book.id)
extra_books.delete()
if book.version == 0:
book.version = 1
book.save()
if self.VERIFY_PAGES:
logger.info(f'Verifing pages: {book}')
book.verify_pages()
else:
book = ComicBook.objects.get(file_name=file.name, directory__isnull=True)
if book.version == 0:
if directory:
book.directory = directory
book.version = 1
book.save()
if self.VERIFY_PAGES:
logger.info(f'Verifing pages: {book}')
book.verify_pages()
except ComicBook.DoesNotExist:
book = ComicBook.process_comic_book(file, directory)
try:
book.generate_thumbnail()
except UnidentifiedImageError:
book.generate_thumbnail(1)
except:
pass

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-09-15 09:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comic', '0046_comicbook_one_comic_name_per_directory'),
]
operations = [
migrations.AddField(
model_name='comicstatus',
name='updated',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.7 on 2022-09-15 09:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comic', '0047_comicstatus_updated'),
]
operations = [
migrations.AddField(
model_name='comicbook',
name='page_count',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.0.7 on 2022-09-15 09:59
from django.db import migrations
from django.db.models import Count
def forwards_func(apps, schema_editor):
books = apps.get_model("comic", "ComicBook")
for book in books.objects.all().annotate(total_pages=Count('comicpage')):
book.page_count = book.total_pages
book.save()
class Migration(migrations.Migration):
dependencies = [
('comic', '0048_comicbook_page_count'),
]
operations = [
migrations.RunPython(forwards_func),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 4.0.7 on 2022-09-15 15:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('comic', '0049_populate_pages'),
]
operations = [
migrations.DeleteModel(
name='ComicPage',
),
]

View File

@@ -3,9 +3,8 @@ import mimetypes
import uuid import uuid
import zipfile import zipfile
from functools import reduce from functools import reduce
from itertools import zip_longest
from pathlib import Path from pathlib import Path
from typing import Optional, List, Union, Tuple, Final from typing import Optional, List, Union, Tuple, Final, IO
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import fitz import fitz
@@ -17,7 +16,6 @@ from django.contrib.auth.models import User
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import models from django.db import models
from django.db.models import UniqueConstraint from django.db.models import UniqueConstraint
from django.db.transaction import atomic
from django_boost.models.fields import AutoOneToOneField from django_boost.models.fields import AutoOneToOneField
from imagekit.models import ProcessedImageField from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill from imagekit.processors import ResizeToFill
@@ -93,7 +91,7 @@ class Directory(models.Model):
self.parent.get_path_items(path_items) self.parent.get_path_items(path_items)
return path_items return path_items
def get_path_objects(self, path_items=None) -> List["Directory"]: def get_path_objects(self, path_items: Optional[List] = None) -> List["Directory"]:
if path_items is None: if path_items is None:
path_items = [] path_items = []
path_items.append(self) path_items.append(self)
@@ -114,6 +112,7 @@ class ComicBook(models.Model):
options={'quality': 60}, options={'quality': 60},
null=True) null=True)
thumbnail_index = models.PositiveIntegerField(default=0) thumbnail_index = models.PositiveIntegerField(default=0)
page_count = models.IntegerField(default=0)
class Meta: class Meta:
constraints = [ constraints = [
@@ -131,13 +130,17 @@ class ComicBook(models.Model):
def type(self) -> str: def type(self) -> str:
return 'ComicBook' return 'ComicBook'
@property
def total(self) -> int:
return self.page_count
def get_pdf(self) -> Path: def get_pdf(self) -> Path:
base_dir = settings.COMIC_BOOK_VOLUME base_dir = settings.COMIC_BOOK_VOLUME
if self.directory: if self.directory:
return Path(base_dir, self.directory.get_path(), self.file_name) return Path(base_dir, self.directory.get_path(), self.file_name)
return Path(base_dir, self.file_name) return Path(base_dir, self.file_name)
def get_image(self, page: int) -> Union[Tuple[io.BytesIO, Image_type], Tuple[bool, bool]]: def get_image(self, page: int) -> Union[Tuple[IO[bytes], str], Tuple[bool, bool]]:
base_dir = settings.COMIC_BOOK_VOLUME base_dir = settings.COMIC_BOOK_VOLUME
if self.directory: if self.directory:
archive_path = Path(base_dir, self.directory.path, self.file_name) archive_path = Path(base_dir, self.directory.path, self.file_name)
@@ -151,14 +154,8 @@ class ComicBook(models.Model):
except zipfile.BadZipfile: except zipfile.BadZipfile:
return False, False return False, False
page_obj = ComicPage.objects.get(Comic=self, index=page) file_name, file_mime = self.get_archive_files(archive)[page]
try: return archive.open(file_name), file_mime
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
except rarfile.NoRarEntry:
self.verify_pages()
page_obj = ComicPage.objects.get(Comic=self, index=page)
out = (archive.open(page_obj.page_file_name), page_obj.content_type)
return out
def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]: def generate_thumbnail_pdf(self, page_index: int = 0) -> Tuple[io.BytesIO, Image_type, str]:
img, pil_data = self._get_pdf_image(page_index if page_index else 0) img, pil_data = self._get_pdf_image(page_index if page_index else 0)
@@ -171,7 +168,7 @@ class ComicBook(models.Model):
img, content_type = self.get_image(page_index) img, content_type = self.get_image(page_index)
pil_data = Image.open(img) pil_data = Image.open(img)
else: else:
for page_number in range(ComicPage.objects.filter(Comic=self).count()): for page_number in range(self.page_count):
try: try:
img, content_type = self.get_image(page_number) img, content_type = self.get_image(page_number)
pil_data = Image.open(img) pil_data = Image.open(img)
@@ -211,10 +208,6 @@ class ComicBook(models.Model):
img.seek(0) img.seek(0)
return img, pil_data return img, pil_data
@property
def page_count(self) -> int:
return ComicPage.objects.filter(Comic=self).count()
@staticmethod @staticmethod
def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]: def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]:
try: try:
@@ -234,14 +227,10 @@ class ComicBook(models.Model):
return comic_file_path return comic_file_path
if archive_type == 'archive': if archive_type == 'archive':
book.verify_pages() book.page_count = len(book.get_archive_files(archive))
elif archive_type == 'pdf': elif archive_type == 'pdf':
with atomic(): book.page_count = archive.page_count
for page_index in range(archive.page_count):
page = ComicPage(
Comic=book, index=page_index, page_file_name=page_index + 1, content_type='application/pdf'
)
page.save()
return book return book
@property @property
@@ -268,56 +257,20 @@ class ComicBook(models.Model):
pass pass
raise NotCompatibleArchive raise NotCompatibleArchive
def get_page_count(self) -> int:
archive, archive_type = self.get_archive()
if archive_type == 'archive':
return len(self.get_archive_files(archive))
elif archive_type == 'pdf':
return archive.page_count
@staticmethod @staticmethod
def get_archive_files(archive) -> List[Tuple[str, str]]: def get_archive_files(archive: Union[zipfile.ZipFile, rarfile.RarFile]) -> List[Tuple[str, str]]:
return [ return [
(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist()) (x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
if not x.endswith('/') and mimetypes.guess_type(x)[0] if not x.endswith('/') and mimetypes.guess_type(x)[0] and not x.endswith('xml')
] ]
def verify_pages(self, pages: Optional["ComicPage"] = None) -> None:
if not pages:
pages = ComicPage.objects.filter(Comic=self)
archive, archive_type = self.get_archive()
if archive_type == 'pdf':
return
archive_files = self.get_archive_files(archive)
index = 0
for a_file, db_file in zip_longest(archive_files, pages):
if not a_file:
db_file.delete()
continue
if not db_file:
ComicPage(
Comic=self,
page_file_name=a_file[0],
index=index,
content_type=a_file[1]
).save()
index += 1
continue
changed = False
if a_file[0] != db_file.page_file_name:
db_file.page_file_name = a_file[0]
changed = True
if a_file[1] != db_file.content_type:
db_file.content_type = a_file[1]
changed = True
if changed:
db_file.save()
index += 1
class ComicPage(models.Model):
Comic = models.ForeignKey(ComicBook, on_delete=models.CASCADE)
index = models.IntegerField()
page_file_name = models.CharField(max_length=200, unique=False)
content_type = models.CharField(max_length=30)
class Meta:
ordering = ['index']
class ComicStatus(models.Model): class ComicStatus(models.Model):
user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE) user = models.ForeignKey(User, unique=False, null=False, on_delete=models.CASCADE)
@@ -326,6 +279,7 @@ class ComicStatus(models.Model):
last_read_page = models.IntegerField(default=0) last_read_page = models.IntegerField(default=0)
unread = models.BooleanField(default=True) unread = models.BooleanField(default=True)
finished = models.BooleanField(default=False) finished = models.BooleanField(default=False)
updated = models.DateTimeField(auto_now=True)
class Meta: class Meta:
constraints = [ constraints = [
@@ -342,9 +296,6 @@ class ComicStatus(models.Model):
) )
# TODO: add support to reference items last being read
class UserMisc(models.Model): class UserMisc(models.Model):
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)

View File

@@ -1,17 +1,19 @@
import mimetypes import mimetypes
import zipfile
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
from typing import NamedTuple, List from typing import NamedTuple, List, Optional, Union
import rarfile
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField, QuerySet
from comic import models from comic import models
from comic.errors import NotCompatibleArchive from comic.errors import NotCompatibleArchive
def generate_directory(user: User, directory=None): def generate_directory(user: User, directory: Optional[models.Directory] = None) -> List[QuerySet]:
dir_path = Path(settings.COMIC_BOOK_VOLUME, directory.path) if directory else settings.COMIC_BOOK_VOLUME dir_path = Path(settings.COMIC_BOOK_VOLUME, directory.path) if directory else settings.COMIC_BOOK_VOLUME
files = [] files = []
@@ -37,7 +39,6 @@ def generate_directory(user: User, directory=None):
models.ComicStatus.objects.bulk_create(new_status) models.ComicStatus.objects.bulk_create(new_status)
file_db_query = file_db_query.annotate( file_db_query = file_db_query.annotate(
total=Count('comicpage', distinct=True),
progress=F('comicstatus__last_read_page') + 1, progress=F('comicstatus__last_read_page') + 1,
finished=F('comicstatus__finished'), finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'), unread=F('comicstatus__unread'),
@@ -60,7 +61,7 @@ def generate_directory(user: User, directory=None):
return files return files
def clean_directories(directories, dir_path, directory=None): def clean_directories(directories: QuerySet, dir_path: Path, directory: Optional[models.Directory] = None) -> None:
dir_db_set = set(Path(settings.COMIC_BOOK_VOLUME, x.path) for x in directories) dir_db_set = set(Path(settings.COMIC_BOOK_VOLUME, x.path) for x in directories)
dir_list = set(x for x in sorted(dir_path.glob('*')) if x.is_dir()) dir_list = set(x for x in sorted(dir_path.glob('*')) if x.is_dir())
# Create new directories db instances # Create new directories db instances
@@ -72,7 +73,7 @@ def clean_directories(directories, dir_path, directory=None):
models.Directory.objects.get(name=stale_directory.name, parent=directory).delete() models.Directory.objects.get(name=stale_directory.name, parent=directory).delete()
def clean_files(files, user, dir_path, directory=None): def clean_files(files: QuerySet, user: User, dir_path: Path, directory: Optional[models.Directory] = None) -> None:
file_list = set(x for x in sorted(dir_path.glob('*')) if x.is_file()) file_list = set(x for x in sorted(dir_path.glob('*')) if x.is_file())
files_db_set = set(Path(dir_path, x.file_name) for x in files) files_db_set = set(Path(dir_path, x.file_name) for x in files)
@@ -80,34 +81,23 @@ def clean_files(files, user, dir_path, directory=None):
books_to_add = [] books_to_add = []
for new_comic in file_list - files_db_set: for new_comic in file_list - files_db_set:
if new_comic.suffix.lower() in settings.SUPPORTED_FILES: if new_comic.suffix.lower() in settings.SUPPORTED_FILES:
books_to_add.append( new_book = models.ComicBook(file_name=new_comic.name, directory=directory)
models.ComicBook(file_name=new_comic.name, directory=directory) archive, archive_type = new_book.get_archive()
) try:
if archive_type == 'archive':
new_book.page_count = len(get_archive_files(archive))
elif archive_type == 'pdf':
new_book.page_count = archive.page_count
except NotCompatibleArchive:
pass
books_to_add.append(new_book)
models.ComicBook.objects.bulk_create(books_to_add) models.ComicBook.objects.bulk_create(books_to_add)
pages_to_add = []
status_to_add = [] status_to_add = []
for book in books_to_add: for book in books_to_add:
status_to_add.append(models.ComicStatus(user=user, comic=book)) status_to_add.append(models.ComicStatus(user=user, comic=book))
try:
archive, archive_type = book.get_archive()
if archive_type == 'archive':
pages_to_add.extend([
models.ComicPage(
Comic=book, index=idx, page_file_name=page.file_name, content_type=page.mime_type
) for idx, page in enumerate(get_archive_files(archive))
])
elif archive_type == 'pdf':
pages_to_add.extend([
models.ComicPage(
Comic=book, index=idx, page_file_name=idx + 1, content_type='application/pdf'
) for idx in range(archive.page_count)
])
except NotCompatibleArchive:
pass
models.ComicStatus.objects.bulk_create(status_to_add) models.ComicStatus.objects.bulk_create(status_to_add)
models.ComicPage.objects.bulk_create(pages_to_add)
# Remove stale comic instances # Remove stale comic instances
for stale_comic in files_db_set - file_list: for stale_comic in files_db_set - file_list:
@@ -119,7 +109,7 @@ class ArchiveFile(NamedTuple):
mime_type: str mime_type: str
def get_archive_files(archive) -> List[ArchiveFile]: def get_archive_files(archive: Union[zipfile.ZipFile, rarfile.RarFile]) -> List[ArchiveFile]:
return [ return [
ArchiveFile(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist()) ArchiveFile(x, mimetypes.guess_type(x)[0]) for x in sorted(archive.namelist())
if not x.endswith('/') and mimetypes.guess_type(x)[0] if not x.endswith('/') and mimetypes.guess_type(x)[0]

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Count, Case, When, F, PositiveSmallIntegerField, FileField, QuerySet from django.db.models import Case, When, F, PositiveSmallIntegerField, FileField, QuerySet
from django.http import FileResponse from django.http import FileResponse
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import viewsets, serializers, mixins, permissions, status, renderers from rest_framework import viewsets, serializers, mixins, permissions, status, renderers
@@ -180,12 +180,6 @@ class GenerateThumbnailViewSet(viewsets.ViewSet):
) )
class PageSerializer(serializers.Serializer):
index = serializers.IntegerField()
page_file_name = serializers.CharField()
content_type = serializers.CharField()
class DirectionSerializer(serializers.Serializer): class DirectionSerializer(serializers.Serializer):
route = serializers.ChoiceField(choices=['read', 'browse']) route = serializers.ChoiceField(choices=['read', 'browse'])
selector = serializers.UUIDField(required=False) selector = serializers.UUIDField(required=False)
@@ -197,7 +191,7 @@ class ReadSerializer(serializers.Serializer):
last_read_page = serializers.IntegerField() last_read_page = serializers.IntegerField()
prev_comic = DirectionSerializer() prev_comic = DirectionSerializer()
next_comic = DirectionSerializer() next_comic = DirectionSerializer()
pages = PageSerializer(many=True) pages = serializers.IntegerField()
class TypeSerializer(serializers.Serializer): class TypeSerializer(serializers.Serializer):
@@ -217,10 +211,13 @@ class ReadViewSet(viewsets.GenericViewSet):
def retrieve(self, request: Request, selector: UUID) -> Response: def retrieve(self, request: Request, selector: UUID) -> Response:
comic = get_object_or_404(models.ComicBook, selector=selector) comic = get_object_or_404(models.ComicBook, selector=selector)
_, _ = models.UserMisc.objects.get_or_create(user=request.user) _, _ = models.UserMisc.objects.get_or_create(user=request.user)
pages = models.ComicPage.objects.filter(Comic=comic)
comic_status, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user) comic_status, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
comic_list = list(models.ComicBook.objects.filter(directory=comic.directory).order_by('file_name')) comic_list = list(models.ComicBook.objects.filter(directory=comic.directory).order_by('file_name'))
comic_index = comic_list.index(comic) comic_index = comic_list.index(comic)
current_page_count = comic.get_page_count()
if comic.page_count != current_page_count:
comic.page_count = current_page_count
comic.save()
try: try:
prev_comic = {'route': 'browse', 'selector': comic.directory.selector} if comic_index == 0 else \ prev_comic = {'route': 'browse', 'selector': comic.directory.selector} if comic_index == 0 else \
{'route': 'read', 'selector': comic_list[comic_index - 1].selector} {'route': 'read', 'selector': comic_list[comic_index - 1].selector}
@@ -238,7 +235,7 @@ class ReadViewSet(viewsets.GenericViewSet):
"last_read_page": comic_status.last_read_page, "last_read_page": comic_status.last_read_page,
"prev_comic": prev_comic, "prev_comic": prev_comic,
"next_comic": next_comic, "next_comic": next_comic,
"pages": pages, "pages": comic.page_count,
} }
serializer = self.serializer_class(data) serializer = self.serializer_class(data)
return Response(serializer.data) return Response(serializer.data)
@@ -269,7 +266,7 @@ class ReadViewSet(viewsets.GenericViewSet):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
comic_status, _ = models.ComicStatus.objects.annotate(page_count=Count('comic__comicpage')) \ comic_status, _ = models.ComicStatus.objects.annotate(page_count=F('comic__page_count')) \
.get_or_create(comic_id=selector, user=request.user) .get_or_create(comic_id=selector, user=request.user)
comic_status.last_read_page = serializer.data['page'] comic_status.last_read_page = serializer.data['page']
comic_status.unread = False comic_status.unread = False
@@ -296,14 +293,14 @@ class PassthroughRenderer(renderers.BaseRenderer): # pylint: disable=too-few-pu
class ImageViewSet(viewsets.ViewSet): class ImageViewSet(viewsets.ViewSet):
queryset = models.ComicPage.objects.all() queryset = models.ComicBook.objects.all()
lookup_field = 'page' lookup_field = 'page'
renderer_classes = [PassthroughRenderer] renderer_classes = [PassthroughRenderer]
@swagger_auto_schema(responses={status.HTTP_200_OK: "A Binary Image response"}) @swagger_auto_schema(responses={status.HTTP_200_OK: "A Binary Image response"})
def retrieve(self, _request: Request, parent_lookup_selector: UUID, page: int) -> FileResponse: def retrieve(self, _request: Request, parent_lookup_selector: UUID, page: int) -> FileResponse:
book = models.ComicBook.objects.get(selector=parent_lookup_selector) book = models.ComicBook.objects.get(selector=parent_lookup_selector)
img, content = book.get_image(int(page)) img, content = book.get_image(int(page) - 1)
self.renderer_classes[0].media_type = content self.renderer_classes[0].media_type = content
return FileResponse(img, content_type=content) return FileResponse(img, content_type=content)
@@ -315,14 +312,17 @@ class StandardResultsSetPagination(PageNumberPagination):
class RecentComicsSerializer(serializers.ModelSerializer): class RecentComicsSerializer(serializers.ModelSerializer):
total_pages = serializers.IntegerField()
unread = serializers.BooleanField() unread = serializers.BooleanField()
finished = serializers.BooleanField() finished = serializers.BooleanField()
last_read_page = serializers.IntegerField() last_read_page = serializers.IntegerField()
class Meta: class Meta:
model = models.ComicBook model = models.ComicBook
fields = ['file_name', 'date_added', 'selector', 'total_pages', 'unread', 'finished', 'last_read_page'] fields = ['file_name', 'date_added', 'selector', 'page_count', 'unread', 'finished', 'last_read_page']
class SearchTextQuerySerializer(serializers.Serializer):
search_text = serializers.CharField(required=False)
class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet): class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet):
@@ -339,7 +339,6 @@ class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet):
query = models.ComicBook.objects.all() query = models.ComicBook.objects.all()
query = query.annotate( query = query.annotate(
total_pages=Count('comicpage'),
unread=Case(When(comicstatus__user=user, then='comicstatus__unread')), unread=Case(When(comicstatus__user=user, then='comicstatus__unread')),
finished=Case(When(comicstatus__user=user, then='comicstatus__finished')), finished=Case(When(comicstatus__user=user, then='comicstatus__finished')),
last_read_page=Case(When(comicstatus__user=user, then='comicstatus__last_read_page')) + 1, last_read_page=Case(When(comicstatus__user=user, then='comicstatus__last_read_page')) + 1,
@@ -353,6 +352,53 @@ class RecentComicsView(mixins.ListModelMixin, viewsets.GenericViewSet):
query = query.order_by('-date_added') query = query.order_by('-date_added')
return query return query
@swagger_auto_schema(query_serializer=SearchTextQuerySerializer())
def list(self, request: Request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
class HistorySerializer(serializers.ModelSerializer):
last_read_time = serializers.DateTimeField()
finished = serializers.BooleanField()
last_read_page = serializers.IntegerField()
class Meta:
model = models.ComicBook
fields = ['file_name', 'selector', 'page_count', 'last_read_time', 'finished', 'last_read_page']
class HistoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
queryset = models.ComicBook.objects.all()
serializer_class = HistorySerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self) -> QuerySet[models.ComicBook]:
user = self.request.user
if "search_text" in self.request.query_params:
query = models.ComicBook.objects.filter(file_name__icontains=self.request.query_params["search_text"])
else:
query = models.ComicBook.objects.all()
query = query.annotate(
last_read_page=Case(When(comicstatus__user=user, then='comicstatus__last_read_page')) + 1,
finished=Case(When(comicstatus__user=user, then='comicstatus__finished')),
unread=Case(When(comicstatus__user=user, then='comicstatus__unread')),
last_read_time=Case(When(comicstatus__user=user, then='comicstatus__updated')),
classification=Case(
When(directory__isnull=True, then=models.Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=models.Directory.Classification.choices)
)
)
query = query.filter(comicstatus__unread=False)
query = query.order_by('-comicstatus__updated')
return query
@swagger_auto_schema(query_serializer=SearchTextQuerySerializer())
def list(self, request: Request, *args, **kwargs) -> Response:
return super().list(request, *args, **kwargs)
class ActionSerializer(serializers.Serializer): class ActionSerializer(serializers.Serializer):
selectors = serializers.ListSerializer(child=serializers.UUIDField()) selectors = serializers.ListSerializer(child=serializers.UUIDField())
@@ -369,7 +415,7 @@ class ActionViewSet(viewsets.GenericViewSet):
if serializer.is_valid(): if serializer.is_valid():
comics = self.get_comics(serializer.data['selectors']) comics = self.get_comics(serializer.data['selectors'])
comic_status = models.ComicStatus.objects.filter(comic__selector__in=comics, user=request.user) comic_status = models.ComicStatus.objects.filter(comic__selector__in=comics, user=request.user)
comic_status = comic_status.annotate(total_pages=Count('comic__comicpage')) comic_status = comic_status.annotate(total_pages=F('comic__page_count'))
status_to_update = [] status_to_update = []
for c_status in comic_status: for c_status in comic_status:
c_status.last_read_page = c_status.total_pages - 1 c_status.last_read_page = c_status.total_pages - 1
@@ -378,8 +424,7 @@ class ActionViewSet(viewsets.GenericViewSet):
status_to_update.append(c_status) status_to_update.append(c_status)
comics.remove(str(c_status.comic_id)) comics.remove(str(c_status.comic_id))
for new_status in comics: for new_status in comics:
comic = models.ComicBook.objects.annotate( comic = models.ComicBook.objects.get(selector=new_status)
total_pages=Count('comicpage')).get(selector=new_status)
obj, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user) obj, _ = models.ComicStatus.objects.get_or_create(comic=comic, user=request.user)
obj.unread = False obj.unread = False
obj.finished = True obj.finished = True

View File

@@ -25,7 +25,7 @@
<li><a class="dropdown-item" @click="updateComic('mark_unread')"><font-awesome-icon icon='book' /> Mark Un-read</a></li> <li><a class="dropdown-item" @click="updateComic('mark_unread')"><font-awesome-icon icon='book' /> Mark Un-read</a></li>
<li><a class="dropdown-item" @click="updateComic('mark_read')"><font-awesome-icon icon='book-open' /> Mark read</a></li> <li><a class="dropdown-item" @click="updateComic('mark_read')"><font-awesome-icon icon='book-open' /> Mark read</a></li>
<li><a class="dropdown-item" v-if="data.type === 'ComicBook'" @click="$emit('markPreviousRead', data.selector)"><font-awesome-icon icon='book' /><font-awesome-icon icon='turn-up' />Mark previous comics read</a></li> <li><a class="dropdown-item" v-if="data.type === 'ComicBook'" @click="$emit('markPreviousRead', data.selector)"><font-awesome-icon icon='book' /><font-awesome-icon icon='turn-up' />Mark previous comics read</a></li>
<li><a class="dropdown-item" v-if="data.type === 'Directory'" data-bs-toggle="modal" :data-bs-target="'#'+data.selector"><font-awesome-icon icon='edit' />Edit comic</a></li> <li><a class="dropdown-item" v-if="data.type === 'Directory'" data-bs-toggle="modal" :data-bs-target="'#id'+data.selector"><font-awesome-icon icon='edit' />Edit comic</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<div class="modal fade" :id="data.selector" tabindex="-1" :aria-labelledby="data.selector+'-label'" aria-hidden="true" > <div class="modal fade" :id="'id'+data.selector" tabindex="-1" :aria-labelledby="data.selector+'-label'" aria-hidden="true" >
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">

View File

@@ -0,0 +1,146 @@
<template>
<div class="container">
<div class="row">
<div class="col d-flex align-items-center">
<form class="form-inline ">
<label class="my-1 px-1" for="selectChoices">Show</label>
<select class="custom-select my-1 mr-sm-2 " id="selectChoices" v-model="this.page_size" @change="this.setPage(this.page)">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<label class="my-1 px-1" for="selectChoices">entries</label>
</form>
</div>
<div class="col d-flex justify-content-end">
<form class="form-inline">
<div class="form-floating">
<input type="text" class="form-control" id="floatingInput" placeholder="name@example.com" v-model="search_text" @keyup="this.debounceInput()">
<label for="floatingInput">Search</label>
</div>
</form>
</div>
</div>
<div class="row">
<caption>
<h2>Reading History</h2>
</caption>
</div>
<div class="row">
<table class="table table-striped table-bordered">
<caption>Recent Comics</caption>
<thead>
<tr>
<th scope="col"></th>
<th scope="col">Comic</th>
<th scope="col">Date Read</th>
<th scope="col">status</th>
</tr>
</thead>
<tbody>
<template v-for="item in comics" :key="item.id">
<tr>
<th scope="row"><font-awesome-icon icon='book' class="" /></th>
<td><router-link :to="{name: 'read', params: { selector: item.selector }}" class="" >{{ item.file_name }}</router-link></td>
<td>{{ timeago(item.last_read_time) }}</td>
<td>{{ get_status(item) }}</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="row">
<div class="col">
Showing page {{ this.page }} of {{ this.page_count }} pages.
</div>
<div class="col d-flex justify-content-end">
<paginate
v-model="this.page"
:page-count="this.page_count"
:click-handler="this.setPage"
:prev-text="'Prev'"
:next-text="'Next'"
:container-class="'pagination '"
>
</paginate>
</div>
</div>
</div>
</template>
<script>
import Paginate from "vuejs-paginate-next";
import api from "@/api";
import * as timeago from "timeago.js";
export default {
name: "HistoryTable",
components: {
Paginate
},
data () {
return {
page: 1,
page_size: 10,
page_count: 2,
search_text: '',
comics: [],
timeout: null,
func_selected: 'choose',
feed_id: ''
}},
methods: {
updateComicList () {
let comic_list_url = '/api/history/'
let params = { params: { page: this.page, page_size: this.page_size } }
if (this.search_text) {
params.params.search_text = this.search_text
}
api.get(comic_list_url, params)
.then(response => {
this.comics = response.data.results
this.page_count = Math.ceil(response.data.count / this.page_size)
})
.catch((error) => {
if (error.response.data.detail === 'Invalid page.') {
this.setPage(1)
} else {
console.log(error)
}
})
},
timeago(input) {
return timeago.format(input)
},
get_status(item) {
if (item.unread || item.unread === null) {
return "Unread"
} else if (item.finished) {
return "Finished"
} else {
return item.last_read_page + ' / ' + item.page_count
}
},
setPage(page) {
this.page = page
this.updateComicList()
},
debounceInput() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.setPage(this.page)
}, 500)
},
},
mounted() {
this.updateComicList()
},
}
</script>
<style scoped>
</style>

View File

@@ -1,15 +1,15 @@
<template> <template>
<div class="reveal" id="comic_box" ref="comic_box" > <div class="reveal" id="comic_box" ref="comic_box" >
<div id="slides_div" class="slides" ref="slides"> <div id="slides_div" class="slides" ref="slides">
<section class="" v-for="page in pages" :key="page.index" :data-menu-title="page.page_file_name" hidden> <section class="" v-for="page in pages" :key="page" :data-menu-title="page" hidden>
<img :data-src="'/api/read/' + selector + '/image/' + page.index + '/'" class="w-100" :alt="page.page_file_name"> <img :data-src="'/api/read/' + selector + '/image/' + page + '/'" class="w-100" :alt="page">
</section> </section>
</div> </div>
</div> </div>
<div class="row navButtons pb-2"> <div class="row navButtons pb-2">
<comic-paginate <comic-paginate
v-model="paginate_page" v-model="paginate_page"
:page_count="pages.length" :page_count="pages"
@setPage="this.setPage" @setPage="this.setPage"
@prevComic="prevComic" @prevComic="prevComic"
@nextComic="nextComic" @nextComic="nextComic"
@@ -35,7 +35,7 @@ export default {
title: '', title: '',
prev_comic: {}, prev_comic: {},
next_comic: {}, next_comic: {},
pages: [], pages: 1,
} }
}, },
props: { props: {
@@ -130,7 +130,8 @@ export default {
plugins: [ ] plugins: [ ]
}).then(() => { }).then(() => {
this.deck.slide(this.current_page) this.deck.slide(this.current_page)
this.deck.on( 'slidechanged', () => { api.put(set_read_url, {page: this.current_page})
this.deck.on( 'slidechanged', (event) => {
this.$refs.comic_box.scrollIntoView({behavior: 'smooth'}) this.$refs.comic_box.scrollIntoView({behavior: 'smooth'})
api.put(set_read_url, {page: event.indexh}) api.put(set_read_url, {page: event.indexh})
}); });

View File

@@ -13,6 +13,9 @@
<li class="nav-item"> <li class="nav-item">
<router-link :to="{name: 'recent'}" class="nav-link" >Recent</router-link> <router-link :to="{name: 'recent'}" class="nav-link" >Recent</router-link>
</li> </li>
<li class="nav-item">
<router-link :to="{name: 'history'}" class="nav-link" >History</router-link>
</li>
<li class="nav-item"> <li class="nav-item">
<router-link :to="{name: 'account'}" class="nav-link" >Account</router-link> <router-link :to="{name: 'account'}" class="nav-link" >Account</router-link>
</li> </li>

View File

@@ -131,7 +131,7 @@ export default {
} else if (item.finished) { } else if (item.finished) {
return "Finished" return "Finished"
} else { } else {
return item.last_read_page + 1 + ' / ' + item.total_pages return item.last_read_page + ' / ' + item.page_count
} }
}, },
setPage(page) { setPage(page) {

View File

@@ -6,6 +6,7 @@ const AccountView = () => import('@/views/AccountView')
const BrowseView = () => import('@/views/BrowseView') const BrowseView = () => import('@/views/BrowseView')
const UserView = () => import('@/views/UserView') const UserView = () => import('@/views/UserView')
const LoginView = () => import('@/views/LoginView') const LoginView = () => import('@/views/LoginView')
const HistoryView = () => import('@/views/HistoryView')
const routes = [ const routes = [
{ {
@@ -37,6 +38,11 @@ const routes = [
name: 'recent', name: 'recent',
component: RecentView component: RecentView
}, },
{
path: '/history',
name: 'history',
component: HistoryView
},
{ {
path: '/account', path: '/account',
name: 'account', name: 'account',

View File

@@ -0,0 +1,24 @@
<template>
<the-breadcrumbs :manual_crumbs="this.crumbs" />
<history-table />
</template>
<script>
import TheBreadcrumbs from "@/components/TheBreadcrumbs";
import HistoryTable from "@/components/HistoryTable";
export default {
name: "HistoryView",
components: {HistoryTable, TheBreadcrumbs},
data () {
return {
crumbs: [
{id: 0, selector: '', name: 'Home'},
{id: 1, selector: '', name: 'History'}
]
}},
}
</script>
<style scoped>
</style>

120
poetry.lock generated
View File

@@ -97,7 +97,7 @@ lxml = ["lxml"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2022.6.15" version = "2022.9.14"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "main" category = "main"
optional = false optional = false
@@ -236,14 +236,14 @@ Django = ">3.2"
[[package]] [[package]]
name = "Django" name = "Django"
version = "4.0.7" version = "4.1.1"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
[package.dependencies] [package.dependencies]
asgiref = ">=3.4.1,<4" asgiref = ">=3.5.2,<4"
sqlparse = ">=0.2.2" sqlparse = ">=0.2.2"
tzdata = {version = "*", markers = "sys_platform == \"win32\""} tzdata = {version = "*", markers = "sys_platform == \"win32\""}
@@ -264,7 +264,7 @@ django = "*"
[[package]] [[package]]
name = "django-boost" name = "django-boost"
version = "2.0" version = "2.1"
description = "Django Extension library" description = "Django Extension library"
category = "main" category = "main"
optional = false optional = false
@@ -314,7 +314,7 @@ tests = ["jinja2 (>=2.9.6)", "mock (==1.0.1)", "pep8 (==1.4.6)", "pytest (<4.0)"
[[package]] [[package]]
name = "django-extensions" name = "django-extensions"
version = "3.2.0" version = "3.2.1"
description = "Extensions for Django" description = "Extensions for Django"
category = "main" category = "main"
optional = false optional = false
@@ -383,7 +383,7 @@ sqlparse = "*"
[[package]] [[package]]
name = "django-sri" name = "django-sri"
version = "0.4.0" version = "0.5.0"
description = "Subresource Integrity for Django" description = "Subresource Integrity for Django"
category = "main" category = "main"
optional = false optional = false
@@ -548,7 +548,7 @@ license = ["ukkonen"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.3" version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "main" category = "main"
optional = false optional = false
@@ -971,11 +971,11 @@ pylint = ">=1.7"
[[package]] [[package]]
name = "PyMuPDF" name = "PyMuPDF"
version = "1.18.19" version = "1.20.2"
description = "Python bindings for the PDF toolkit and renderer MuPDF" description = "Python bindings for the PDF toolkit and renderer MuPDF"
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = ">=3.7"
[[package]] [[package]]
name = "pyOpenSSL" name = "pyOpenSSL"
@@ -1016,11 +1016,11 @@ six = ">=1.5"
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "0.20.0" version = "0.21.0"
description = "Read key-value pairs from a .env file and set them as environment variables" description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.7"
[package.extras] [package.extras]
cli = ["click (>=5.0)"] cli = ["click (>=5.0)"]
@@ -1169,7 +1169,7 @@ python-versions = ">=3.6,<4.0"
[[package]] [[package]]
name = "traitlets" name = "traitlets"
version = "5.3.0" version = "5.4.0"
description = "" description = ""
category = "dev" category = "dev"
optional = false optional = false
@@ -1236,7 +1236,7 @@ ua-parser = ">=0.10.0"
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.16.4" version = "20.16.5"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev" category = "dev"
optional = false optional = false
@@ -1261,11 +1261,14 @@ python-versions = "*"
[[package]] [[package]]
name = "Werkzeug" name = "Werkzeug"
version = "2.0.3" version = "2.2.2"
description = "The comprehensive WSGI web application library." description = "The comprehensive WSGI web application library."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras] [package.extras]
watchdog = ["watchdog"] watchdog = ["watchdog"]
@@ -1292,7 +1295,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "85edbd1fe3ad53c66f95b372ad7e1891058831431f08de21f11197047ce8c51b" content-hash = "8adcd11c8f30ed42dc6d187b3a3f5b0f020180c362d13670b39467523732819d"
[metadata.files] [metadata.files]
appnope = [ appnope = [
@@ -1328,8 +1331,8 @@ beautifulsoup4 = [
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
] ]
certifi = [ certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, {file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"},
] ]
cffi = [ cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -1514,16 +1517,16 @@ dj-database-url = [
{file = "dj_database_url-1.0.0-py3-none-any.whl", hash = "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39"}, {file = "dj_database_url-1.0.0-py3-none-any.whl", hash = "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39"},
] ]
Django = [ Django = [
{file = "Django-4.0.7-py3-none-any.whl", hash = "sha256:41bd65a9e5f8a89cdbfa7a7bba45cd7431ae89e750af82dea8a35fd1a7ecbe66"}, {file = "Django-4.1.1-py3-none-any.whl", hash = "sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e"},
{file = "Django-4.0.7.tar.gz", hash = "sha256:9c6d5ad36be798e562ddcaa6b17b1c3ff2d3c4f529a47432b69fb9a30f847461"}, {file = "Django-4.1.1.tar.gz", hash = "sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e"},
] ]
django-appconf = [ django-appconf = [
{file = "django-appconf-1.0.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"}, {file = "django-appconf-1.0.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"},
{file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"}, {file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"},
] ]
django-boost = [ django-boost = [
{file = "django_boost-2.0-py3-none-any.whl", hash = "sha256:266c22a5a7bdae480cfdc337b11073e1e906521a4f9c5b3dbb7e15bfeb916065"}, {file = "django_boost-2.1-py3-none-any.whl", hash = "sha256:c59450d082bb6f7c130d0fcbf5cb9346c183d5d78856966838b17e02901e31b9"},
{file = "django_boost-2.0.tar.gz", hash = "sha256:6d6c2d7c34d54cfdbb6232d755967eabaaf8db5395bc003637e87a7f0e329016"}, {file = "django_boost-2.1.tar.gz", hash = "sha256:a7d8defc2ca0eeebd08636abe58a21094f43fcc7c0ff020f9f8deec82d53a39f"},
] ]
django-bootstrap4 = [ django-bootstrap4 = [
{file = "django-bootstrap4-22.2.tar.gz", hash = "sha256:57bcbce53530ffd57a1c8bda74c8f3b56c859e085fb1f52a0e3e3a4f982f0960"}, {file = "django-bootstrap4-22.2.tar.gz", hash = "sha256:57bcbce53530ffd57a1c8bda74c8f3b56c859e085fb1f52a0e3e3a4f982f0960"},
@@ -1538,8 +1541,8 @@ django-csp = [
{file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"}, {file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"},
] ]
django-extensions = [ django-extensions = [
{file = "django-extensions-3.2.0.tar.gz", hash = "sha256:7dc7cd1da50d83b76447a58f5d7e5c8e6cd83f21e9b7e5f97e6b644f4d4e21a6"}, {file = "django-extensions-3.2.1.tar.gz", hash = "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4"},
{file = "django_extensions-3.2.0-py3-none-any.whl", hash = "sha256:4c234a7236e9e41c17d9036f6dae7a3a9b212527105b8a0d24b2459b267825f0"}, {file = "django_extensions-3.2.1-py3-none-any.whl", hash = "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09"},
] ]
django-filter = [ django-filter = [
{file = "django-filter-22.1.tar.gz", hash = "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5"}, {file = "django-filter-22.1.tar.gz", hash = "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5"},
@@ -1558,8 +1561,8 @@ django-silk = [
{file = "django_silk-5.0.1-py3-none-any.whl", hash = "sha256:9dad85e783fcaaa1c97bebfa7ea01899428ded55b517d363f25ba87e43b5ce50"}, {file = "django_silk-5.0.1-py3-none-any.whl", hash = "sha256:9dad85e783fcaaa1c97bebfa7ea01899428ded55b517d363f25ba87e43b5ce50"},
] ]
django-sri = [ django-sri = [
{file = "django-sri-0.4.0.tar.gz", hash = "sha256:8a21b9808c351fe28d731ac1af9043b2525ba93d883aab888424cd8b121bbef1"}, {file = "django-sri-0.5.0.tar.gz", hash = "sha256:9d9042a01f9314d308f8b40ea084768f55a182e2a82e2ea53412ca5f4433a28e"},
{file = "django_sri-0.4.0-py3-none-any.whl", hash = "sha256:44e0fb6a33d767008098293014d89b380ee6ec65ffe034a89dcff8f199c5abb0"}, {file = "django_sri-0.5.0-py3-none-any.whl", hash = "sha256:c2621bed5660b5ac19ecf39b49e83df73625ba43d58fb5a35833f2100162819d"},
] ]
django-webpack-loader = [ django-webpack-loader = [
{file = "django-webpack-loader-1.6.0.tar.gz", hash = "sha256:a29418ff41690035be10d2c94a0655dab099009c68b898839813d6a109b14d71"}, {file = "django-webpack-loader-1.6.0.tar.gz", hash = "sha256:a29418ff41690035be10d2c94a0655dab099009c68b898839813d6a109b14d71"},
@@ -1610,8 +1613,8 @@ identify = [
{file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"}, {file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"},
] ]
idna = [ idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
] ]
inflection = [ inflection = [
{file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"},
@@ -1917,30 +1920,27 @@ pylint-plugin-utils = [
{file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"}, {file = "pylint_plugin_utils-0.7-py3-none-any.whl", hash = "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535"},
] ]
PyMuPDF = [ PyMuPDF = [
{file = "PyMuPDF-1.18.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee8cc8aadaa818c9a5e2fb2a944c99a98822a7a3bc618d9c5d32f126874c0635"}, {file = "PyMuPDF-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:18077adb58b7004bb396f952d27c89c6bf5dc521b4056ba4f95772c7e900a57a"},
{file = "PyMuPDF-1.18.19-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:03ebf6fce6889df4708061a499b912909ead5e7bf1066f05b94458dcf164e3c3"}, {file = "PyMuPDF-1.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a82438b44723e7dcb47d156cf0299fcf3d09e970ee469081c3f9d79b8c40ce3"},
{file = "PyMuPDF-1.18.19-cp310-cp310-win32.whl", hash = "sha256:f4bc63b0696c2f276703fadf3232d7ccaa01ab7548d1541fbc3762c573d3e1b5"}, {file = "PyMuPDF-1.20.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73dbf73ac1c5cbf99dec903c98bc18eb30c8fe1d703ec5531296a5308700f001"},
{file = "PyMuPDF-1.18.19-cp310-cp310-win_amd64.whl", hash = "sha256:c2fc348061c14c79e546a207088ba8bf676dc8f1d302cb94cefe53d53c2a7808"}, {file = "PyMuPDF-1.20.2-cp310-cp310-win32.whl", hash = "sha256:1df02cca0fbb64c3dce4d6094f9ad91991d768ebe0c0efdee85294c7959db7c3"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a795f40654a98459007148fa0660de5a9e1e1e0d12dc5d56bbdea481a1ec8fbc"}, {file = "PyMuPDF-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:8074f005d247bbb6b43c0c1eba2a316d97cbf8345b456d37ee97c9cd2a7398cc"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c802bdc3fdf690470a490cbcbf026895bb497f8049d33df47e1c64da939d46b3"}, {file = "PyMuPDF-1.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4045847e830598269be448bda21dc8ea332fce974c4cf3b1b6ca0d6231f2de0d"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5044f9447686874f442a676776615b48b6a04d63a68dd99da21ad7a0efd5d13a"}, {file = "PyMuPDF-1.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5824103cf2ea3b492cb5b3c66ab16d61db65363ebdfb7ed28a2af93cb48dee4f"},
{file = "PyMuPDF-1.18.19-cp36-cp36m-win_amd64.whl", hash = "sha256:7a7fc4b4069934d7663dbd2bcf698f123b9bb0c4b4e25383a549692bee56f466"}, {file = "PyMuPDF-1.20.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c50b8408dbbb921cfce7145ac0ae0e150955f9b2deed8cdddf03e830ca1e6a2"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc2df761b59ff06d16db7a5ce6d19bcab365efd4391eac96dab30ddda3a09f87"}, {file = "PyMuPDF-1.20.2-cp37-cp37m-win32.whl", hash = "sha256:1bd2004a64f110dac255a86ec45245f17e84d81cf6f8f67608e45455cd1b5697"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:af2c6934eb8dc803c958d9742c7b64aa7d6287f764844f296dfdfe9e576e4df4"}, {file = "PyMuPDF-1.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:acadb4e61776cc2d6dd0196675db324411fccd64e8d68399dc8e6ca2cb943f49"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d772141c9e007b887da54da5677e13975305bd85ae833eec35b923bed35ea9c8"}, {file = "PyMuPDF-1.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b07f6c389c690f28c79830a14c294bbc1679bd31edc4b6a42511c415434cbd"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-win32.whl", hash = "sha256:ea96c0129d6be8a289b80e7f0a5f6842e09c3dcb568a170297afb256cc387146"}, {file = "PyMuPDF-1.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d29a7db4b15dcb7632bb6afa197f12f95244ab15f7c73ccb219e3f7df4e79fa"},
{file = "PyMuPDF-1.18.19-cp37-cp37m-win_amd64.whl", hash = "sha256:e7f1897600f7a56073b1370a1709f68b00c8700c4ef945bcffde1a09f58c2746"}, {file = "PyMuPDF-1.20.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2897fb4160b5fe2f80739ca9e101677e5694b5dcfda8026b0eb0b7e2db5615"},
{file = "PyMuPDF-1.18.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c43b849c1daef36dc83667726518af3178c831c053e3e946e1989b5fb477bca"}, {file = "PyMuPDF-1.20.2-cp38-cp38-win32.whl", hash = "sha256:2af5b16d2f7f78a8800906e30d650dbdbf38209e988e0796b57d1a938a59209a"},
{file = "PyMuPDF-1.18.19-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:47a1588ce2296517019e8ba64279fe4cc03592d96fb1fc726ed9d0f5f01b5ef8"}, {file = "PyMuPDF-1.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:071732c5139150f1f7ba95d25c0b69919dee8494adc88a914872da5272b974d7"},
{file = "PyMuPDF-1.18.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc8e1f0c5fb54cb91ad3b630ef623834cb2e4b110c33eabccad9b8609053b20d"}, {file = "PyMuPDF-1.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5afa38414c3cf10daaa627f3fc7e8653efbbc2e5f58335a37e743a5c52698d0"},
{file = "PyMuPDF-1.18.19-cp38-cp38-win32.whl", hash = "sha256:230d130f22a91da0a897c6b7e9b3523d24dc512ee32568a40d0df0e2cf2562c0"}, {file = "PyMuPDF-1.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddf7d617fba1b345d9aee8d533888e340ffa3f784910453608bc5541089023c"},
{file = "PyMuPDF-1.18.19-cp38-cp38-win_amd64.whl", hash = "sha256:7c0f145445b3ef8eb45794bf0f86fcd278696da5a6b879666b690e4856f5e481"}, {file = "PyMuPDF-1.20.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c0ee0df665849f8686981fcc8f836afac2302bb6a7aaf330e73fd5694017d9d"},
{file = "PyMuPDF-1.18.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c12e1870841a8746f023a64ef6d8a44fa02324d17252c3e182f639a9d5530261"}, {file = "PyMuPDF-1.20.2-cp39-cp39-win32.whl", hash = "sha256:e7214cf1870238d39ca008eda7c612b1db7228d7e913cb1a028e7a28175bef41"},
{file = "PyMuPDF-1.18.19-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:69308316b031aace9e490e9010d6c5334129eed47b47804da72d5d07eb5b2f9a"}, {file = "PyMuPDF-1.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:e9cb960e234498a4e91e4b56e2e71f4c56fd0045c7a71315df8e03a33212dbeb"},
{file = "PyMuPDF-1.18.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616533cff3605c22a327488b4470f9929da675b006d7a1498f8d7ed8bf91f386"}, {file = "PyMuPDF-1.20.2.tar.gz", hash = "sha256:02eedf01f57c6bafb5e8667cea0088a2d2522643c47100f1908bec3a68a84888"},
{file = "PyMuPDF-1.18.19-cp39-cp39-win32.whl", hash = "sha256:f407dbeecdefdfd46b9df7b083da71558143859216dc9891c40ea9cd2cf49a04"},
{file = "PyMuPDF-1.18.19-cp39-cp39-win_amd64.whl", hash = "sha256:439b972026fbe8636aed0fe9d2cabb321542fa92bc48cd4c96dbdd2508fc41ee"},
{file = "PyMuPDF-1.18.19.tar.gz", hash = "sha256:ecc684e9c45bd4072f538cc42998cfda4d00f066ba009226e8a212b112d9992c"},
] ]
pyOpenSSL = [ pyOpenSSL = [
{file = "pyOpenSSL-22.0.0-py2.py3-none-any.whl", hash = "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"}, {file = "pyOpenSSL-22.0.0-py2.py3-none-any.whl", hash = "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0"},
@@ -1955,8 +1955,8 @@ python-dateutil = [
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
] ]
python-dotenv = [ python-dotenv = [
{file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
{file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},
] ]
pytz = [ pytz = [
{file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"},
@@ -2074,8 +2074,8 @@ tomlkit = [
{file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"},
] ]
traitlets = [ traitlets = [
{file = "traitlets-5.3.0-py3-none-any.whl", hash = "sha256:65fa18961659635933100db8ca120ef6220555286949774b9cfc106f941d1c7a"}, {file = "traitlets-5.4.0-py3-none-any.whl", hash = "sha256:93663cc8236093d48150e2af5e2ed30fc7904a11a6195e21bab0408af4e6d6c8"},
{file = "traitlets-5.3.0.tar.gz", hash = "sha256:0bb9f1f9f017aa8ec187d8b1b2a7a6626a2a1d877116baba52a129bfa124f8e2"}, {file = "traitlets-5.4.0.tar.gz", hash = "sha256:3f2c4e435e271592fe4390f1746ea56836e3a080f84e7833f0f801d9613fec39"},
] ]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
@@ -2102,16 +2102,16 @@ user-agents = [
{file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"}, {file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"},
] ]
virtualenv = [ virtualenv = [
{file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"}, {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
{file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"}, {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
] ]
wcwidth = [ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
] ]
Werkzeug = [ Werkzeug = [
{file = "Werkzeug-2.0.3-py3-none-any.whl", hash = "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8"}, {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"},
{file = "Werkzeug-2.0.3.tar.gz", hash = "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"}, {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"},
] ]
win32-setctime = [ win32-setctime = [
{file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},

View File

@@ -3,30 +3,30 @@ line_length = 119
[tool.poetry] [tool.poetry]
name = "cbwebreader" name = "cbwebreader"
version = "1.0.6" version = "1.1.0"
description = "CBR/Z Web Reader" description = "CBR/Z Web Reader"
authors = ["ajurna <ajurna@gmail.com>"] authors = ["ajurna <ajurna@gmail.com>"]
license = "Creative Commons Attribution-ShareAlike 4.0 International License" license = "Creative Commons Attribution-ShareAlike 4.0 International License"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
Django = "4.0.7" Django = "4.1.1"
gunicorn = "^20.0.4" gunicorn = "^20.0.4"
dj-database-url = "^1.0.0" dj-database-url = "^1.0.0"
python-dotenv = "^0.20.0" python-dotenv = "^0.21.0"
loguru = "^0.6.0" loguru = "^0.6.0"
django-silk = "^5.0.0" django-silk = "^5.0.0"
mysqlclient = "^2.0.1" mysqlclient = "^2.0.1"
psycopg2 = "^2.8.6" psycopg2 = "^2.8.6"
rarfile = "^4.0" rarfile = "^4.0"
django-extensions = "^3.2.0" django-extensions = "^3.2.1"
Pillow = "^9.1.1" Pillow = "^9.1.1"
django-imagekit = "^4.0.2" django-imagekit = "^4.0.2"
PyMuPDF = "~1.18" PyMuPDF = "~1.20.2"
django-bootstrap4 = "^22.1" django-bootstrap4 = "^22.1"
django-csp = "^3.7" django-csp = "^3.7"
django-boost = "^2.0" django-boost = "^2.1"
django-sri = "^0.4.0" django-sri = "^0.5.0"
django-permissions-policy = "^4.9.0" django-permissions-policy = "^4.9.0"
djangorestframework = "^3.13.1" djangorestframework = "^3.13.1"
django-filter = "^22.1" django-filter = "^22.1"
@@ -40,7 +40,7 @@ flake8-annotations = "^2.9.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = "^0.971" mypy = "^0.971"
Werkzeug = "<2.1" Werkzeug = "^2.2"
pyOpenSSL = "^22.0.0" pyOpenSSL = "^22.0.0"
ipython = "^8.4.0" ipython = "^8.4.0"
coverage = "^6.2" coverage = "^6.2"

View File

@@ -2,25 +2,25 @@ asgiref==3.5.2 ; python_version >= "3.10" and python_version < "4.0"
attrs==22.1.0 ; python_version >= "3.10" and python_version < "4.0" attrs==22.1.0 ; python_version >= "3.10" and python_version < "4.0"
autopep8==1.7.0 ; python_version >= "3.10" and python_version < "4.0" autopep8==1.7.0 ; python_version >= "3.10" and python_version < "4.0"
beautifulsoup4==4.11.1 ; python_version >= "3.10" and python_version < "4.0" beautifulsoup4==4.11.1 ; python_version >= "3.10" and python_version < "4.0"
certifi==2022.6.15 ; python_version >= "3.10" and python_version < "4" certifi==2022.9.14 ; python_version >= "3.10" and python_version < "4"
charset-normalizer==2.1.1 ; python_version >= "3.10" and python_version < "4" charset-normalizer==2.1.1 ; python_version >= "3.10" and python_version < "4"
colorama==0.4.5 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" colorama==0.4.5 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
coreapi==2.3.3 ; python_version >= "3.10" and python_version < "4.0" coreapi==2.3.3 ; python_version >= "3.10" and python_version < "4.0"
coreschema==0.0.4 ; python_version >= "3.10" and python_version < "4.0" coreschema==0.0.4 ; python_version >= "3.10" and python_version < "4.0"
dj-database-url==1.0.0 ; python_version >= "3.10" and python_version < "4.0" dj-database-url==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
django-appconf==1.0.5 ; python_version >= "3.10" and python_version < "4.0" django-appconf==1.0.5 ; python_version >= "3.10" and python_version < "4.0"
django-boost==2.0 ; python_version >= "3.10" and python_version < "4.0" django-boost==2.1 ; python_version >= "3.10" and python_version < "4.0"
django-bootstrap4==22.2 ; python_version >= "3.10" and python_version < "4.0" django-bootstrap4==22.2 ; python_version >= "3.10" and python_version < "4.0"
django-cors-headers==3.13.0 ; python_version >= "3.10" and python_version < "4.0" django-cors-headers==3.13.0 ; python_version >= "3.10" and python_version < "4.0"
django-csp==3.7 ; python_version >= "3.10" and python_version < "4.0" django-csp==3.7 ; python_version >= "3.10" and python_version < "4.0"
django-extensions==3.2.0 ; python_version >= "3.10" and python_version < "4.0" django-extensions==3.2.1 ; python_version >= "3.10" and python_version < "4.0"
django-filter==22.1 ; python_version >= "3.10" and python_version < "4.0" django-filter==22.1 ; python_version >= "3.10" and python_version < "4.0"
django-imagekit==4.1.0 ; python_version >= "3.10" and python_version < "4.0" django-imagekit==4.1.0 ; python_version >= "3.10" and python_version < "4.0"
django-permissions-policy==4.13.0 ; python_version >= "3.10" and python_version < "4.0" django-permissions-policy==4.13.0 ; python_version >= "3.10" and python_version < "4.0"
django-silk==5.0.1 ; python_version >= "3.10" and python_version < "4.0" django-silk==5.0.1 ; python_version >= "3.10" and python_version < "4.0"
django-sri==0.4.0 ; python_version >= "3.10" and python_version < "4.0" django-sri==0.5.0 ; python_version >= "3.10" and python_version < "4.0"
django-webpack-loader==1.6.0 ; python_version >= "3.10" and python_version < "4.0" django-webpack-loader==1.6.0 ; python_version >= "3.10" and python_version < "4.0"
django==4.0.7 ; python_version >= "3.10" and python_version < "4.0" django==4.1.1 ; python_version >= "3.10" and python_version < "4.0"
djangorestframework-simplejwt==5.2.0 ; python_version >= "3.10" and python_version < "4.0" djangorestframework-simplejwt==5.2.0 ; python_version >= "3.10" and python_version < "4.0"
djangorestframework==3.13.1 ; python_version >= "3.10" and python_version < "4.0" djangorestframework==3.13.1 ; python_version >= "3.10" and python_version < "4.0"
drf-extensions==0.7.1 ; python_version >= "3.10" and python_version < "4.0" drf-extensions==0.7.1 ; python_version >= "3.10" and python_version < "4.0"
@@ -29,7 +29,7 @@ flake8-annotations==2.9.1 ; python_version >= "3.10" and python_version < "4.0"
flake8==5.0.4 ; python_version >= "3.10" and python_version < "4.0" flake8==5.0.4 ; python_version >= "3.10" and python_version < "4.0"
gprof2dot==2022.7.29 ; python_version >= "3.10" and python_version < "4.0" gprof2dot==2022.7.29 ; python_version >= "3.10" and python_version < "4.0"
gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0" gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0"
idna==3.3 ; python_version >= "3.10" and python_version < "4" idna==3.4 ; python_version >= "3.10" and python_version < "4"
inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0" inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0"
itypes==1.2.0 ; python_version >= "3.10" and python_version < "4.0" itypes==1.2.0 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0"
@@ -44,10 +44,10 @@ psycopg2==2.9.3 ; python_version >= "3.10" and python_version < "4.0"
pycodestyle==2.9.1 ; python_version >= "3.10" and python_version < "4.0" pycodestyle==2.9.1 ; python_version >= "3.10" and python_version < "4.0"
pyflakes==2.5.0 ; python_version >= "3.10" and python_version < "4.0" pyflakes==2.5.0 ; python_version >= "3.10" and python_version < "4.0"
pyjwt==2.4.0 ; python_version >= "3.10" and python_version < "4.0" pyjwt==2.4.0 ; python_version >= "3.10" and python_version < "4.0"
pymupdf==1.18.19 ; python_version >= "3.10" and python_version < "4.0" pymupdf==1.20.2 ; python_version >= "3.10" and python_version < "4.0"
pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "4.0" pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
python-dotenv==0.20.0 ; python_version >= "3.10" and python_version < "4.0" python-dotenv==0.21.0 ; python_version >= "3.10" and python_version < "4.0"
pytz==2022.2.1 ; python_version >= "3.10" and python_version < "4.0" pytz==2022.2.1 ; python_version >= "3.10" and python_version < "4.0"
rarfile==4.0 ; python_version >= "3.10" and python_version < "4.0" rarfile==4.0 ; python_version >= "3.10" and python_version < "4.0"
requests==2.28.1 ; python_version >= "3.10" and python_version < "4" requests==2.28.1 ; python_version >= "3.10" and python_version < "4"