mirror of
https://github.com/ajurna/cbwebreader.git
synced 2025-12-06 06:17:17 +00:00
Merge pull request #77
* added timestamp to comicstatus. * added timestamp to comicstatus.
This commit is contained in:
@@ -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')
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
18
comic/migrations/0047_comicstatus_updated.py
Normal file
18
comic/migrations/0047_comicstatus_updated.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
comic/migrations/0048_comicbook_page_count.py
Normal file
18
comic/migrations/0048_comicbook_page_count.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
22
comic/migrations/0049_populate_pages.py
Normal file
22
comic/migrations/0049_populate_pages.py
Normal 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),
|
||||||
|
]
|
||||||
16
comic/migrations/0050_delete_comicpage.py
Normal file
16
comic/migrations/0050_delete_comicpage.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
146
frontend/src/components/HistoryTable.vue
Normal file
146
frontend/src/components/HistoryTable.vue
Normal 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>
|
||||||
@@ -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})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
24
frontend/src/views/HistoryView.vue
Normal file
24
frontend/src/views/HistoryView.vue
Normal 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
120
poetry.lock
generated
@@ -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"},
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user