Classification (#32)

* added some code cleanup for views.py

* added some code cleanup for views.py

* fixed comics not working in the base directory.
This commit is contained in:
2021-05-11 15:52:56 +01:00
committed by GitHub
parent 443e43e3f0
commit ce38340a22
21 changed files with 355 additions and 105 deletions

View File

@@ -40,6 +40,7 @@ INSTALLED_APPS = (
"comic_auth",
'django_extensions',
'imagekit',
'django_boost',
)
MIDDLEWARE = [

View File

@@ -47,5 +47,5 @@ class ComicStatusAdmin(admin.ModelAdmin):
@admin.register(UserMisc)
class UserMiscAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'feed_id')
list_display = ('user', 'feed_id', 'allowed_to_read')
list_filter = ('user',)

View File

@@ -3,6 +3,8 @@ from os import path
from django import forms
from django.contrib.auth.models import User
from comic.models import Directory
class InitialSetupForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
@@ -87,28 +89,23 @@ class EditUserForm(forms.Form):
required=False,
widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
)
email = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
email = forms.EmailField(widget=forms.TextInput(attrs={"class": "form-control"}))
password = forms.CharField(
required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
)
# TODO: allow setting superuser on users
allowed_to_read = forms.ChoiceField(choices=Directory.Classification.choices)
@staticmethod
def get_initial_values(user):
out = {"username": user.username, "email": user.email}
out = {"username": user.username, "email": user.email, "allowed_to_read": user.usermisc.allowed_to_read}
return out
def clean_email(self):
data = self.cleaned_data["email"]
user = User.objects.get(username=self.cleaned_data["username"])
if data == user.email:
return data
if User.objects.filter(email=data).exists():
raise forms.ValidationError("Email Address is in use")
return data
def clean_password(self):
data = self.cleaned_data["password"]
if len(data) < 8 & len(data) != 0:
raise forms.ValidationError("Password is too short")
return data
return data
class DirectoryEditForm(forms.Form):
classification = forms.ChoiceField(choices=Directory.Classification.choices)

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2 on 2021-05-06 12:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comic', '0024_auto_20210422_0855'),
]
operations = [
migrations.AddField(
model_name='directory',
name='classification',
field=models.PositiveSmallIntegerField(choices=[(0, 'G'), (1, 'PG'), (2, '12'), (3, '15'), (4, '18')], default=4),
),
migrations.AddField(
model_name='usermisc',
name='allowed_to_read',
field=models.PositiveSmallIntegerField(choices=[(0, 'G'), (1, 'PG'), (2, '12'), (3, '15'), (4, '18')], default=4),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2 on 2021-05-06 12:50
from django.conf import settings
from django.db import migrations
import django.db.models.deletion
import django_boost.models.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comic', '0025_auto_20210506_1342'),
]
operations = [
migrations.AlterField(
model_name='usermisc',
name='user',
field=django_boost.models.fields.AutoOneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.2 on 2021-05-06 12:56
from django.conf import settings
from django.db import migrations
import django.db.models.deletion
import django_boost.models.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comic', '0026_alter_usermisc_user'),
]
operations = [
migrations.RemoveField(
model_name='usermisc',
name='id',
),
migrations.AlterField(
model_name='usermisc',
name='user',
field=django_boost.models.fields.AutoOneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -3,8 +3,7 @@ import mimetypes
import uuid
import zipfile
from functools import reduce
from itertools import zip_longest
from os import listdir
from itertools import zip_longest, chain
from pathlib import Path
from typing import Optional, List, Union, Tuple
@@ -18,6 +17,7 @@ from django.db import models
from django.db.transaction import atomic
from django.templatetags.static import static
from django.utils.http import urlsafe_base64_encode
from django_boost.models.fields import AutoOneToOneField
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill
@@ -28,6 +28,13 @@ if settings.UNRAR_TOOL:
class Directory(models.Model):
class Classification(models.IntegerChoices):
C_G = 0, 'G'
C_PG = 1, 'PG'
C_12 = 2, '12'
C_15 = 3, '15'
C_18 = 4, '18'
name = models.CharField(max_length=100)
parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE)
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
@@ -40,6 +47,7 @@ class Directory(models.Model):
on_delete=models.SET_NULL,
related_name='directory_thumbnail_issue')
thumbnail_index = models.PositiveIntegerField(default=0)
classification = models.PositiveSmallIntegerField(choices=Classification.choices, default=Classification.C_18)
class Meta:
ordering = ['name']
@@ -108,6 +116,10 @@ class Directory(models.Model):
def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes)
def set_classification(self, form_data):
self.classification = form_data['classification']
self.save()
class ComicBook(models.Model):
file_name = models.TextField()
@@ -144,7 +156,7 @@ class ComicBook(models.Model):
def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes)
def get_pdf(self):
def get_pdf(self) -> Path:
base_dir = settings.COMIC_BOOK_VOLUME
return Path(base_dir, self.directory.get_path(), self.file_name)
@@ -209,6 +221,7 @@ class ComicBook(models.Model):
self.save()
def _get_pdf_image(self, page_index: int):
# noinspection PyTypeChecker
doc = fitz.open(self.get_pdf())
page = doc[page_index]
pix = page.get_pixmap()
@@ -219,7 +232,6 @@ class ComicBook(models.Model):
img.seek(0)
return img, pil_data
def is_last_page(self, page):
if (self.page_count - 1) == page:
return True
@@ -265,9 +277,9 @@ class ComicBook(models.Model):
book = ComicBook.objects.get(file_name=prev_comic, directory__isnull=True)
except ComicBook.DoesNotExist:
if self.directory:
book = ComicBook.process_comic_book(prev_comic, self.directory)
book = ComicBook.process_comic_book(Path(prev_comic), self.directory)
else:
book = ComicBook.process_comic_book(prev_comic)
book = ComicBook.process_comic_book(Path(prev_comic))
cs, _ = ComicStatus.objects.get_or_create(comic=book, user=user)
comic_path = urlsafe_base64_encode(book.selector.bytes)
@@ -290,9 +302,9 @@ class ComicBook(models.Model):
book = ComicBook.objects.get(file_name=next_comic, directory__isnull=True)
except ComicBook.DoesNotExist:
if self.directory:
book = ComicBook.process_comic_book(next_comic, self.directory)
book = ComicBook.process_comic_book(Path(next_comic), self.directory)
else:
book = ComicBook.process_comic_book(next_comic)
book = ComicBook.process_comic_book(Path(next_comic))
except ComicBook.MultipleObjectsReturned:
if self.directory:
books = ComicBook.objects.filter(file_name=next_comic, directory=self.directory).order_by('id')
@@ -353,21 +365,21 @@ class ComicBook(models.Model):
with atomic():
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'
Comic=book, index=page_index, page_file_name=page_index + 1, content_type='application/pdf'
)
page.save()
return book
@staticmethod
def get_ordered_dir_list(folder):
def get_ordered_dir_list(folder: Path) -> List[str]:
directories = []
files = []
for item in listdir(folder):
if Path(folder, item).is_dir():
for item in folder.glob('*'):
if item.is_dir():
directories.append(item)
else:
files.append(item)
return sorted(directories) + sorted(files)
return [x.name for x in chain(sorted(directories), sorted(files))]
@property
def get_archive_path(self):
@@ -388,6 +400,7 @@ class ComicBook(models.Model):
pass
try:
# noinspection PyUnresolvedReferences
return fitz.open(str(archive_path)), 'pdf'
except RuntimeError:
pass
@@ -475,5 +488,8 @@ class ComicStatus(models.Model):
class UserMisc(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
feed_id = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
allowed_to_read = models.PositiveSmallIntegerField(default=Directory.Classification.C_18,
choices=Directory.Classification.choices)

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
@@ -48,7 +49,7 @@
{% endif %}
</a>
<div class="card-body">
<h5 class="card-title">
<h5 class="card-title {{ file.selector }}">
{% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}" class="search-name">
{% elif file.item_type == 'ComicBook' %}
@@ -63,7 +64,7 @@
<div class="progress-bar" role="progressbar" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</p>
<div class="btn-group" role="group" aria-label="Comic Actions">
<div class="btn-group w-100" role="group" aria-label="Comic Actions">
<button type="button" class="btn btn-primary comic_action" title="Mark Un-Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_unread"><i class="fas fa-book"></i></button>
<button type="button" class="btn btn-primary comic_action" title="Mark Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_read"><i class="fas fa-book-open"></i></button>
<div class="btn-group" role="group">
@@ -75,25 +76,48 @@
<button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_read"><i class="fas fa-book-open">Mark Read</i></button>
{% if file.item_type != 'Directory' %}
<button type="button" class="btn btn-primary dropdown-item comic_action" title="Mark Previous Read" selector="{{ file.selector }}" itemtype="{{ file.item_type }}" comic_action="mark_previous"><i class="fas fa-book"><i class="fas fa-arrow-up">Mark Previous Read</i></i></button>
{% else %}
<button type="button" class="btn btn-primary dropdown-item modal-button" title="Edit Comic" data-toggle="modal" data-target="#editModal" selector="{{ file.selector }}" itemtype="{{ file.item_type }}"><i class="fas fa-edit">Edit Comic</i></button>
{% endif %}
{# <button type="button" class="btn btn-primary dropdown-item" title="Edit Comic"><i class="fas fa-edit">Edit Comic</i></button>#}
</div>
</div>
</div>
{% if file.total_unread and file.item_type == 'Directory' %}
<span class="badge rounded-pill bg-primary card-badge">{{ file.total_unread }}</span>
{% endif %}
</div>
{% if file.total_unread and file.item_type == 'Directory' %}
<span class="badge rounded-pill bg-primary unread-badge">{{ file.total_unread }}</span>
{% endif %}
<span class="badge rounded-pill bg-warning {{ file.selector }} classification-badge" classification="{{ file.obj.classification }}">{{ file.obj.get_classification_display }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% csrf_token %}
{% bootstrap_form form %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="save_button">Save changes</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script type="text/javascript" src="{% static "js/comic_list.min.js" %}"></script>
{{ js_urls|json_script:'js_urls' }}
<script type="text/javascript" src="{% static "js/comic_list.min.js" %}"></script>
{% endblock %}

View File

@@ -11,6 +11,7 @@
<th>Username</th>
<th>Email</th>
<th>Superuser</th>
<th>Classification</th>
</tr>
</thead>
<tbody data-link="row" class="rowlink">
@@ -20,6 +21,7 @@
<td><a href="{% url 'user_details' user.id %}">{{user.username}}</a></td>
<td>{{user.email}}</td>
<td>{{user.is_superuser}}</td>
<td>{{ user.usermisc.get_allowed_to_read_display }}</td>
</tr>
{% endfor %}
</tbody>

View File

View File

@@ -0,0 +1,4 @@
from django import template
register = template.Library()

View File

@@ -17,7 +17,7 @@ urlpatterns = [
path("recent/", views.recent_comics, name="recent_comics"),
path("recent/json/", views.recent_comics_json, name="recent_comics_json"),
path("edit/", views.comic_edit, name="comic_edit"),
path("feed/<int:user_selector>/", feeds.RecentComics()),
path("feed/<user_selector>/", feeds.RecentComics()),
path("<directory_selector>/", views.comic_list, name="comic_list"),
path("<directory_selector>/thumb", views.directory_thumbnail, name="directory_thumbnail"),
path("action/<operation>/<item_type>/<selector>/", views.perform_action, name="perform_action")

View File

@@ -4,7 +4,8 @@ from pathlib import Path
from typing import Union
from django.conf import settings
from django.db.models import Count, Q, F
from django.contrib.auth.models import User
from django.db.models import Count, Q, F, Case, When, PositiveSmallIntegerField
from django.utils.http import urlsafe_base64_encode
from .models import ComicBook, Directory, ComicStatus
@@ -112,7 +113,7 @@ class DirFile:
self.name = self.obj.file_name
def generate_directory(user, directory=False):
def generate_directory(user: User, directory=False):
"""
:type user: User
:type directory: Directory
@@ -150,7 +151,12 @@ def generate_directory(user, directory=False):
total_read=F('comicstatus__last_read_page'),
finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'),
user=F('comicstatus__user')
user=F('comicstatus__user'),
classification=Case(
When(directory__isnull=True, then=Directory.Classification.C_18),
default=F('directory__classification'),
output_field=PositiveSmallIntegerField(choices=Directory.Classification.choices)
)
).filter(Q(user__isnull=True) | Q(user=user.id))
for directory_obj in dir_list_obj:
@@ -170,6 +176,7 @@ def generate_directory(user, directory=False):
directory_obj.total = 0
directory_obj.total_read = 0
files.append(DirFile(directory_obj))
files = [file for file in files if file.obj.classification <= user.usermisc.allowed_to_read]
for file_name in file_list:
if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]:

View File

@@ -1,22 +1,21 @@
import json
import uuid
from PIL import Image
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db.models import Max, Count, F
from django.db.transaction import atomic
from django.http import HttpResponse, FileResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST
from .forms import AccountForm, AddUserForm, EditUserForm, InitialSetupForm
from .models import ComicBook, ComicPage, ComicStatus, Directory, UserMisc
from .forms import AccountForm, AddUserForm, EditUserForm, InitialSetupForm, DirectoryEditForm
from .models import ComicBook, ComicPage, ComicStatus, Directory
from .util import (
Menu,
generate_breadcrumbs_from_menu,
@@ -47,6 +46,7 @@ def comic_list(request, directory_selector=False):
breadcrumbs = generate_breadcrumbs_from_path()
files = generate_directory(request.user, directory)
form = DirectoryEditForm()
return render(
request,
@@ -56,14 +56,18 @@ def comic_list(request, directory_selector=False):
"menu": Menu(request.user, "Browse"),
"title": title,
"files": files,
"selector": directory_selector if directory_selector else 'None'
"form": form,
"selector": directory_selector if directory_selector else 'None',
'js_urls': {
"perform_action": reverse('perform_action', args=('operation', 'item_type', 'selector'))
}
},
)
@login_required
def perform_action(request, operation, item_type, selector):
if operation not in ['mark_read', 'mark_unread', 'mark_previous']:
if operation not in ['mark_read', 'mark_unread', 'mark_previous', 'set_classification']:
return HttpResponse(400)
elif operation == 'mark_previous' and item_type == 'Directory':
return HttpResponse(422)
@@ -74,20 +78,29 @@ def perform_action(request, operation, item_type, selector):
for book in ComicBook.objects.filter(directory__isnull=True):
getattr(book, operation)(request.user)
return HttpResponse(204)
else:
return HttpResponse(400)
if operation == 'set_classification':
form = DirectoryEditForm(request.POST)
if form.is_valid() and item_type == 'Directory':
pass
else:
return HttpResponse(400)
if item_type == 'ComicBook':
book = get_object_or_404(ComicBook, selector=selector_uuid)
getattr(book, operation)(request.user)
return HttpResponse(204)
elif item_type == 'Directory':
directory = get_object_or_404(Directory, selector=selector_uuid)
getattr(directory, operation)(request.user)
if operation == 'set_classification':
getattr(directory, operation)(form.cleaned_data)
else:
getattr(directory, operation)(request.user)
return HttpResponse(204)
@login_required
def recent_comics(request):
feed_id, _ = UserMisc.objects.get_or_create(user=request.user)
return render(
request,
"comic/recent_comics.html",
@@ -95,7 +108,7 @@ def recent_comics(request):
"breadcrumbs": generate_breadcrumbs_from_menu([("Recent", "/comic/recent/")]),
"menu": Menu(request.user, "Recent"),
"title": "Recent Comics",
"feed_id": urlsafe_base64_encode(feed_id.feed_id.bytes),
"feed_id": urlsafe_base64_encode(request.user.usermisc.feed_id.bytes),
},
)
@@ -197,7 +210,7 @@ def account_page(request):
@user_passes_test(lambda u: u.is_superuser)
def users_page(request):
users = User.objects.all()
users = User.objects.all().select_related('usermisc')
crumbs = [("Users", "/comic/settings/users/")]
context = {
"users": users,
@@ -221,6 +234,8 @@ def user_config_page(request, user_id):
if form.cleaned_data["email"] != user.email:
user.email = form.cleaned_data["email"]
success_message.append("Email Updated.</br>")
user.usermisc.allowed_to_read = form.cleaned_data['allowed_to_read']
user.usermisc.save()
user.save()
else:
form = EditUserForm(initial=EditUserForm.get_initial_values(user))
@@ -248,7 +263,6 @@ def user_add_page(request):
user = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"])
user.set_password(form.cleaned_data["password"])
user.save()
UserMisc.objects.create(user=user)
success_message = "User {} created.".format(user.username)
else:
@@ -269,14 +283,10 @@ def user_add_page(request):
def read_comic(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
try:
book = ComicBook.objects.get(selector=selector)
except ComicBook.DoesNotExist:
Directory.objects.get(selector=selector)
return redirect('comic_list', directory_selector=comic_selector)
except Directory.DoesNotExist:
return HttpResponse(status=404)
book = get_object_or_404(ComicBook, selector=selector)
if book.directory:
if book.directory.classification > request.user.usermisc.allowed_to_read:
return redirect('index')
pages = ComicPage.objects.filter(Comic=book)
@@ -318,33 +328,42 @@ def set_read_page(request, comic_selector, page):
@xframe_options_sameorigin
@login_required
def get_image(_, comic_selector, page):
def get_image(request, comic_selector, page):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
if book.directory:
if book.directory.classification > request.user.usermisc.allowed_to_read:
return HttpResponse(status=401)
img, content = book.get_image(int(page))
return FileResponse(img, content_type=content)
@xframe_options_sameorigin
@login_required
def comic_thumbnail(_, comic_selector):
def comic_thumbnail(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
if book.directory.classification > request.user.usermisc.allowed_to_read:
return HttpResponse(status=401)
return redirect(book.get_thumbnail_url())
@xframe_options_sameorigin
@login_required
def directory_thumbnail(_, directory_selector):
def directory_thumbnail(request, directory_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
folder = Directory.objects.get(selector=selector)
if folder.classification > request.user.usermisc.allowed_to_read:
return HttpResponse(status=401)
return redirect(folder.get_thumbnail_url())
@login_required
def get_pdf(_, comic_selector):
def get_pdf(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=selector)
if book.directory.classification > request.user.usermisc.allowed_to_read:
return HttpResponse(status=401)
return FileResponse(open(book.get_pdf(), 'rb'), content_type='application/pdf')

45
poetry.lock generated
View File

@@ -107,6 +107,18 @@ python-versions = "*"
[package.dependencies]
django = "*"
[[package]]
name = "django-boost"
version = "1.7.2"
description = "Django Extension library"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Django = ">=2.0"
user-agents = ">=2.0"
[[package]]
name = "django-bootstrap4"
version = "3.0.0"
@@ -443,6 +455,14 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "ua-parser"
version = "0.10.0"
description = "Python port of Browserscope's user agent parser"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.4"
@@ -456,6 +476,17 @@ secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "cer
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
brotli = ["brotlipy (>=0.6.0)"]
[[package]]
name = "user-agents"
version = "2.2.0"
description = "A library to identify devices (phones, tablets) and their capabilities by parsing browser user agent strings."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
ua-parser = ">=0.10.0"
[[package]]
name = "win32-setctime"
version = "1.0.3"
@@ -470,7 +501,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "71642aa577156d70c6033dbc260a2ab03d247a17d9b0b0500a9c9a0e0228fd68"
content-hash = "8364532c96609a5598f24f5e77f2b647763fafc8052c1d466dabc154a90c6d09"
[metadata.files]
asgiref = [
@@ -564,6 +595,10 @@ django-appconf = [
{file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"},
{file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"},
]
django-boost = [
{file = "django_boost-1.7.2-py3-none-any.whl", hash = "sha256:b2460f8613920cdb309cb5c27a0a18d8fb83812f6019c6bb2218f82b33a67ea3"},
{file = "django_boost-1.7.2.tar.gz", hash = "sha256:ca80641314f75446ba815ed9632c64ede025c72bc18ec59af89abce97769a65f"},
]
django-bootstrap4 = [
{file = "django-bootstrap4-3.0.0.tar.gz", hash = "sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b"},
{file = "django_bootstrap4-3.0.0-py3-none-any.whl", hash = "sha256:76a52fb22a8d3dbb2f7609b21908ce863e941a4462be079bf1d12025e551af37"},
@@ -849,10 +884,18 @@ typing-extensions = [
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
ua-parser = [
{file = "ua-parser-0.10.0.tar.gz", hash = "sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"},
{file = "ua_parser-0.10.0-py2.py3-none-any.whl", hash = "sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a"},
]
urllib3 = [
{file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
{file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
]
user-agents = [
{file = "user-agents-2.2.0.tar.gz", hash = "sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26"},
{file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"},
]
win32-setctime = [
{file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"},
{file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"},

View File

@@ -27,6 +27,7 @@ django-imagekit = "^4.0.2"
PyMuPDF = "^1.18.12"
django-bootstrap4 = "^3.0.0"
django-csp = "^3.7"
django-boost = "^1.7.2"
[tool.poetry.dev-dependencies]
mypy = "^0.812"

View File

@@ -76,6 +76,9 @@ dj-database-url==0.5.0 \
django-appconf==1.0.4 \
--hash=sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380 \
--hash=sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06
django-boost==1.7.2 \
--hash=sha256:b2460f8613920cdb309cb5c27a0a18d8fb83812f6019c6bb2218f82b33a67ea3 \
--hash=sha256:ca80641314f75446ba815ed9632c64ede025c72bc18ec59af89abce97769a65f
django-bootstrap4==3.0.0; python_version >= "3.6" \
--hash=sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b \
--hash=sha256:76a52fb22a8d3dbb2f7609b21908ce863e941a4462be079bf1d12025e551af37
@@ -271,9 +274,15 @@ sqlparse==0.4.1; python_version >= "3.6" \
toml==0.10.2; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5" \
--hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
--hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
ua-parser==0.10.0 \
--hash=sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033 \
--hash=sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a
urllib3==1.26.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" \
--hash=sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df \
--hash=sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937
user-agents==2.2.0 \
--hash=sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26 \
--hash=sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7
win32-setctime==1.0.3; sys_platform == "win32" and python_version >= "3.5" \
--hash=sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e \
--hash=sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b

View File

@@ -5,10 +5,17 @@
.card_list_card {
width: 200px;
}
.card .card-badge {
.card .unread-badge {
position:absolute;
top:10px;
left:10px;
padding:5px;
color:white;
}
.card .classification-badge {
position:absolute;
top:10px;
right: 10px;
padding:5px;
color:black;
}

View File

@@ -1 +1 @@
#comic_list caption{caption-side:top}.card_list_card{width:200px}.card .card-badge{position:absolute;top:10px;left:10px;padding:5px;color:#fff}
#comic_list caption{caption-side:top}.card_list_card{width:200px}.card .unread-badge{position:absolute;top:10px;left:10px;padding:5px;color:#fff}.card .classification-badge{position:absolute;top:10px;right:10px;padding:5px;color:#000}

View File

@@ -1,45 +1,48 @@
var qsRegex;
var buttonFilter;
var $grid = $('.comic-container').isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
filter: function() {
var $this = $(this);
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
return searchResult && buttonResult;
}
});
$('#filters').on( 'click', 'button', function() {
if (typeof $( this ).attr('data-filter') === "undefined") {
let qsRegex;
let buttonFilter;
const js_urls = JSON.parse(document.getElementById('js_urls').textContent)
}else {
buttonFilter = $( this ).attr('data-filter');
sessionStorage.setItem(window.location.href+"button", buttonFilter);
$grid.isotope();
}
});
let $grid = $('.comic-container').isotope({
itemSelector: '.grid-item',
layoutMode: 'fitRows',
filter: function() {
let $this = $(this);
let searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
let buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
return searchResult && buttonResult;
}
});
var $quicksearch = $('#quicksearch').keyup( debounce( function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
$grid.isotope();
}) );
$('#filters').on( 'click', 'button', function() {
if (typeof $( this ).attr('data-filter') === "undefined") {
}else {
buttonFilter = $( this ).attr('data-filter');
sessionStorage.setItem(window.location.href+"button", buttonFilter);
$grid.isotope();
}
});
let $quicksearch = $('#quicksearch').keyup( debounce( function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
$grid.isotope();
}) );
// debounce so filtering doesn't happen every millisecond
function debounce( fn, threshold ) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout( timeout );
var args = arguments;
var _this = this;
function delayed() {
fn.apply( _this, args );
}
timeout = setTimeout( delayed, threshold );
};
function debounce( fn, threshold ) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout( timeout );
var args = arguments;
var _this = this;
function delayed() {
fn.apply( _this, args );
}
timeout = setTimeout( delayed, threshold );
};
}
setInterval(function (){
$grid.isotope();
}, 1000)
@@ -84,3 +87,49 @@ comic_action_elements.forEach(el => el.addEventListener('click', event => {
let action = target.attr('comic_action')
comic_action(selector, item_type, action)
}));
let modal_buttons = document.getElementsByClassName('modal-button')
modal_buttons.forEach(el => el.addEventListener('click', event => {
let target = $(event.target).closest('button')
let selector = target.attr('selector')
let modal = $('#editModal')
modal.attr('selector', selector)
modal.attr('itemtype', target.attr('itemtype'))
let title = $('#editModalLabel')
let title_source = $('.card-title.'+selector)
title.text(title_source.text())
let classification = $('select[name="classification"]')
let classification_value = $('.classification-badge.'+selector)
classification.val(classification_value.attr('classification'))
}))
let save_button = document.getElementById('save_button')
save_button.addEventListener('click', function (event){
let modal = $('#editModal')
let selector = modal.attr('selector')
let itemtype = modal.attr('itemtype')
let classification = $('select[name="classification"]')
let classification_badge = $('.classification-badge.'+selector)
let action_url = js_urls.perform_action.replace('operation', 'set_classification').replace('item_type', itemtype).replace('selector', selector)
$.ajax({
type: "POST",
url: action_url,
data: {
classification: classification.val(),
csrfmiddlewaretoken: $('input[name="csrfmiddlewaretoken"]').attr('value')
},
success: function (ev){
classification_badge.text($('select[name="classification"] option:selected').text())
modal.modal('hide')
},
})
})

View File

@@ -1 +1 @@
var qsRegex;var buttonFilter;var $grid=$(".comic-container").isotope({itemSelector:".grid-item",layoutMode:"fitRows",filter:function(){var $this=$(this);var searchResult=qsRegex?$this.text().match(qsRegex):true;var buttonResult=buttonFilter?$this.is(buttonFilter):true;return searchResult&&buttonResult}});$("#filters").on("click","button",function(){if(typeof $(this).attr("data-filter")==="undefined"){}else{buttonFilter=$(this).attr("data-filter");sessionStorage.setItem(window.location.href+"button",buttonFilter);$grid.isotope()}});var $quicksearch=$("#quicksearch").keyup(debounce(function(){qsRegex=new RegExp($quicksearch.val(),"gi");sessionStorage.setItem(window.location.href+"text",$quicksearch.val());$grid.isotope()}));function debounce(fn,threshold){var timeout;threshold=threshold||100;return function debounced(){clearTimeout(timeout);var args=arguments;var _this=this;function delayed(){fn.apply(_this,args)}timeout=setTimeout(delayed,threshold)}}setInterval(function(){$grid.isotope()},1e3);let field=document.getElementById("quicksearch");if(sessionStorage.getItem(window.location.href+"text")||sessionStorage.getItem(window.location.href+"button")){field.value=sessionStorage.getItem(window.location.href+"text");qsRegex=new RegExp($quicksearch.val(),"gi");buttonFilter=sessionStorage.getItem(window.location.href+"button");$grid.isotope()}field.addEventListener("change",function(){});function comic_action(selector,item_type,action){$.ajax({url:"/comic/action/"+action+"/"+item_type+"/"+selector+"/",success:function(){window.location.reload()}})}$(".progress-bar").each(function(index){let bar=$(this);bar.css("width",bar.attr("aria-valuenow")+"%")});let comic_action_elements=document.getElementsByClassName("comic_action");comic_action_elements.forEach(el=>el.addEventListener("click",event=>{let target=$(event.target).closest("button");let selector=target.attr("selector");let item_type=target.attr("itemtype");let action=target.attr("comic_action");comic_action(selector,item_type,action)}));
let qsRegex;let buttonFilter;const js_urls=JSON.parse(document.getElementById("js_urls").textContent);let $grid=$(".comic-container").isotope({itemSelector:".grid-item",layoutMode:"fitRows",filter:function(){let $this=$(this);let searchResult=qsRegex?$this.text().match(qsRegex):true;let buttonResult=buttonFilter?$this.is(buttonFilter):true;return searchResult&&buttonResult}});$("#filters").on("click","button",function(){if(typeof $(this).attr("data-filter")==="undefined"){}else{buttonFilter=$(this).attr("data-filter");sessionStorage.setItem(window.location.href+"button",buttonFilter);$grid.isotope()}});let $quicksearch=$("#quicksearch").keyup(debounce(function(){qsRegex=new RegExp($quicksearch.val(),"gi");sessionStorage.setItem(window.location.href+"text",$quicksearch.val());$grid.isotope()}));function debounce(fn,threshold){var timeout;threshold=threshold||100;return function debounced(){clearTimeout(timeout);var args=arguments;var _this=this;function delayed(){fn.apply(_this,args)}timeout=setTimeout(delayed,threshold)}}setInterval(function(){$grid.isotope()},1e3);let field=document.getElementById("quicksearch");if(sessionStorage.getItem(window.location.href+"text")||sessionStorage.getItem(window.location.href+"button")){field.value=sessionStorage.getItem(window.location.href+"text");qsRegex=new RegExp($quicksearch.val(),"gi");buttonFilter=sessionStorage.getItem(window.location.href+"button");$grid.isotope()}field.addEventListener("change",function(){});function comic_action(selector,item_type,action){$.ajax({url:"/comic/action/"+action+"/"+item_type+"/"+selector+"/",success:function(){window.location.reload()}})}$(".progress-bar").each(function(index){let bar=$(this);bar.css("width",bar.attr("aria-valuenow")+"%")});let comic_action_elements=document.getElementsByClassName("comic_action");comic_action_elements.forEach(el=>el.addEventListener("click",event=>{let target=$(event.target).closest("button");let selector=target.attr("selector");let item_type=target.attr("itemtype");let action=target.attr("comic_action");comic_action(selector,item_type,action)}));let modal_buttons=document.getElementsByClassName("modal-button");modal_buttons.forEach(el=>el.addEventListener("click",event=>{let target=$(event.target).closest("button");let selector=target.attr("selector");let modal=$("#editModal");modal.attr("selector",selector);modal.attr("itemtype",target.attr("itemtype"));let title=$("#editModalLabel");let title_source=$(".card-title."+selector);title.text(title_source.text());let classification=$('select[name="classification"]');let classification_value=$(".classification-badge."+selector);classification.val(classification_value.attr("classification"))}));let save_button=document.getElementById("save_button");save_button.addEventListener("click",function(event){let modal=$("#editModal");let selector=modal.attr("selector");let itemtype=modal.attr("itemtype");let classification=$('select[name="classification"]');let classification_badge=$(".classification-badge."+selector);let action_url=js_urls.perform_action.replace("operation","set_classification").replace("item_type",itemtype).replace("selector",selector);$.ajax({type:"POST",url:action_url,data:{classification:classification.val(),csrfmiddlewaretoken:$('input[name="csrfmiddlewaretoken"]').attr("value")},success:function(ev){classification_badge.text($('select[name="classification"] option:selected').text());modal.modal("hide")}})});