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", "comic_auth",
'django_extensions', 'django_extensions',
'imagekit', 'imagekit',
'django_boost',
) )
MIDDLEWARE = [ MIDDLEWARE = [

View File

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

View File

@@ -3,6 +3,8 @@ from os import path
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from comic.models import Directory
class InitialSetupForm(forms.Form): class InitialSetupForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"}))
@@ -87,28 +89,23 @@ class EditUserForm(forms.Form):
required=False, required=False,
widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}), 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( password = forms.CharField(
required=False, widget=forms.PasswordInput(attrs={"class": "form-control"}) 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 @staticmethod
def get_initial_values(user): 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 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): def clean_password(self):
data = self.cleaned_data["password"] data = self.cleaned_data["password"]
if len(data) < 8 & len(data) != 0: if len(data) < 8 & len(data) != 0:
raise forms.ValidationError("Password is too short") 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 uuid
import zipfile import zipfile
from functools import reduce from functools import reduce
from itertools import zip_longest from itertools import zip_longest, chain
from os import listdir
from pathlib import Path from pathlib import Path
from typing import Optional, List, Union, Tuple from typing import Optional, List, Union, Tuple
@@ -18,6 +17,7 @@ from django.db import models
from django.db.transaction import atomic from django.db.transaction import atomic
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.http import urlsafe_base64_encode from django.utils.http import urlsafe_base64_encode
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
@@ -28,6 +28,13 @@ if settings.UNRAR_TOOL:
class Directory(models.Model): 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) name = models.CharField(max_length=100)
parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE) parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE)
selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True) selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True)
@@ -40,6 +47,7 @@ class Directory(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='directory_thumbnail_issue') related_name='directory_thumbnail_issue')
thumbnail_index = models.PositiveIntegerField(default=0) thumbnail_index = models.PositiveIntegerField(default=0)
classification = models.PositiveSmallIntegerField(choices=Classification.choices, default=Classification.C_18)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -108,6 +116,10 @@ class Directory(models.Model):
def url_safe_selector(self): def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes) return urlsafe_base64_encode(self.selector.bytes)
def set_classification(self, form_data):
self.classification = form_data['classification']
self.save()
class ComicBook(models.Model): class ComicBook(models.Model):
file_name = models.TextField() file_name = models.TextField()
@@ -144,7 +156,7 @@ class ComicBook(models.Model):
def url_safe_selector(self): def url_safe_selector(self):
return urlsafe_base64_encode(self.selector.bytes) return urlsafe_base64_encode(self.selector.bytes)
def get_pdf(self): def get_pdf(self) -> Path:
base_dir = settings.COMIC_BOOK_VOLUME base_dir = settings.COMIC_BOOK_VOLUME
return Path(base_dir, self.directory.get_path(), self.file_name) return Path(base_dir, self.directory.get_path(), self.file_name)
@@ -209,6 +221,7 @@ class ComicBook(models.Model):
self.save() self.save()
def _get_pdf_image(self, page_index: int): def _get_pdf_image(self, page_index: int):
# noinspection PyTypeChecker
doc = fitz.open(self.get_pdf()) doc = fitz.open(self.get_pdf())
page = doc[page_index] page = doc[page_index]
pix = page.get_pixmap() pix = page.get_pixmap()
@@ -219,7 +232,6 @@ class ComicBook(models.Model):
img.seek(0) img.seek(0)
return img, pil_data return img, pil_data
def is_last_page(self, page): def is_last_page(self, page):
if (self.page_count - 1) == page: if (self.page_count - 1) == page:
return True return True
@@ -265,9 +277,9 @@ class ComicBook(models.Model):
book = ComicBook.objects.get(file_name=prev_comic, directory__isnull=True) book = ComicBook.objects.get(file_name=prev_comic, directory__isnull=True)
except ComicBook.DoesNotExist: except ComicBook.DoesNotExist:
if self.directory: if self.directory:
book = ComicBook.process_comic_book(prev_comic, self.directory) book = ComicBook.process_comic_book(Path(prev_comic), self.directory)
else: 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) cs, _ = ComicStatus.objects.get_or_create(comic=book, user=user)
comic_path = urlsafe_base64_encode(book.selector.bytes) 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) book = ComicBook.objects.get(file_name=next_comic, directory__isnull=True)
except ComicBook.DoesNotExist: except ComicBook.DoesNotExist:
if self.directory: if self.directory:
book = ComicBook.process_comic_book(next_comic, self.directory) book = ComicBook.process_comic_book(Path(next_comic), self.directory)
else: else:
book = ComicBook.process_comic_book(next_comic) book = ComicBook.process_comic_book(Path(next_comic))
except ComicBook.MultipleObjectsReturned: except ComicBook.MultipleObjectsReturned:
if self.directory: if self.directory:
books = ComicBook.objects.filter(file_name=next_comic, directory=self.directory).order_by('id') books = ComicBook.objects.filter(file_name=next_comic, directory=self.directory).order_by('id')
@@ -359,15 +371,15 @@ class ComicBook(models.Model):
return book return book
@staticmethod @staticmethod
def get_ordered_dir_list(folder): def get_ordered_dir_list(folder: Path) -> List[str]:
directories = [] directories = []
files = [] files = []
for item in listdir(folder): for item in folder.glob('*'):
if Path(folder, item).is_dir(): if item.is_dir():
directories.append(item) directories.append(item)
else: else:
files.append(item) files.append(item)
return sorted(directories) + sorted(files) return [x.name for x in chain(sorted(directories), sorted(files))]
@property @property
def get_archive_path(self): def get_archive_path(self):
@@ -388,6 +400,7 @@ class ComicBook(models.Model):
pass pass
try: try:
# noinspection PyUnresolvedReferences
return fitz.open(str(archive_path)), 'pdf' return fitz.open(str(archive_path)), 'pdf'
except RuntimeError: except RuntimeError:
pass pass
@@ -475,5 +488,8 @@ class ComicStatus(models.Model):
class UserMisc(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) 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" %} {% extends "base.html" %}
{% load bootstrap4 %}
{% load static %} {% load static %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
@@ -48,7 +49,7 @@
{% endif %} {% endif %}
</a> </a>
<div class="card-body"> <div class="card-body">
<h5 class="card-title"> <h5 class="card-title {{ file.selector }}">
{% if file.item_type == 'Directory' %} {% if file.item_type == 'Directory' %}
<a href="{% url "comic_list" file.selector %}" class="search-name"> <a href="{% url "comic_list" file.selector %}" class="search-name">
{% elif file.item_type == 'ComicBook' %} {% 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 class="progress-bar" role="progressbar" aria-valuenow="{{ file.percent }}" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
</p> </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 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> <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"> <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> <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' %} {% 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> <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 %} {% 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> </div>
</div>
</div> </div>
{% if file.total_unread and file.item_type == 'Directory' %} {% if file.total_unread and file.item_type == 'Directory' %}
<span class="badge rounded-pill bg-primary card-badge">{{ file.total_unread }}</span> <span class="badge rounded-pill bg-primary unread-badge">{{ file.total_unread }}</span>
{% endif %} {% endif %}
<span class="badge rounded-pill bg-warning {{ file.selector }} classification-badge" classification="{{ file.obj.classification }}">{{ file.obj.get_classification_display }}</span>
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</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 %} {% endblock %}
{% block script %} {% block script %}
{{ js_urls|json_script:'js_urls' }}
<script type="text/javascript" src="{% static "js/comic_list.min.js" %}"></script> <script type="text/javascript" src="{% static "js/comic_list.min.js" %}"></script>
{% endblock %} {% endblock %}

View File

@@ -11,6 +11,7 @@
<th>Username</th> <th>Username</th>
<th>Email</th> <th>Email</th>
<th>Superuser</th> <th>Superuser</th>
<th>Classification</th>
</tr> </tr>
</thead> </thead>
<tbody data-link="row" class="rowlink"> <tbody data-link="row" class="rowlink">
@@ -20,6 +21,7 @@
<td><a href="{% url 'user_details' user.id %}">{{user.username}}</a></td> <td><a href="{% url 'user_details' user.id %}">{{user.username}}</a></td>
<td>{{user.email}}</td> <td>{{user.email}}</td>
<td>{{user.is_superuser}}</td> <td>{{user.is_superuser}}</td>
<td>{{ user.usermisc.get_allowed_to_read_display }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </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/", views.recent_comics, name="recent_comics"),
path("recent/json/", views.recent_comics_json, name="recent_comics_json"), path("recent/json/", views.recent_comics_json, name="recent_comics_json"),
path("edit/", views.comic_edit, name="comic_edit"), 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>/", views.comic_list, name="comic_list"),
path("<directory_selector>/thumb", views.directory_thumbnail, name="directory_thumbnail"), path("<directory_selector>/thumb", views.directory_thumbnail, name="directory_thumbnail"),
path("action/<operation>/<item_type>/<selector>/", views.perform_action, name="perform_action") 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 typing import Union
from django.conf import settings 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 django.utils.http import urlsafe_base64_encode
from .models import ComicBook, Directory, ComicStatus from .models import ComicBook, Directory, ComicStatus
@@ -112,7 +113,7 @@ class DirFile:
self.name = self.obj.file_name self.name = self.obj.file_name
def generate_directory(user, directory=False): def generate_directory(user: User, directory=False):
""" """
:type user: User :type user: User
:type directory: Directory :type directory: Directory
@@ -150,7 +151,12 @@ def generate_directory(user, directory=False):
total_read=F('comicstatus__last_read_page'), total_read=F('comicstatus__last_read_page'),
finished=F('comicstatus__finished'), finished=F('comicstatus__finished'),
unread=F('comicstatus__unread'), 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)) ).filter(Q(user__isnull=True) | Q(user=user.id))
for directory_obj in dir_list_obj: for directory_obj in dir_list_obj:
@@ -170,6 +176,7 @@ def generate_directory(user, directory=False):
directory_obj.total = 0 directory_obj.total = 0
directory_obj.total_read = 0 directory_obj.total_read = 0
files.append(DirFile(directory_obj)) 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: for file_name in file_list:
if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]: if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]:

View File

@@ -1,22 +1,21 @@
import json import json
import uuid import uuid
from PIL import Image
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User 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.models import Max, Count, F
from django.db.transaction import atomic from django.db.transaction import atomic
from django.http import HttpResponse, FileResponse from django.http import HttpResponse, FileResponse
from django.shortcuts import get_object_or_404, redirect, render 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.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from .forms import AccountForm, AddUserForm, EditUserForm, InitialSetupForm from .forms import AccountForm, AddUserForm, EditUserForm, InitialSetupForm, DirectoryEditForm
from .models import ComicBook, ComicPage, ComicStatus, Directory, UserMisc from .models import ComicBook, ComicPage, ComicStatus, Directory
from .util import ( from .util import (
Menu, Menu,
generate_breadcrumbs_from_menu, generate_breadcrumbs_from_menu,
@@ -47,6 +46,7 @@ def comic_list(request, directory_selector=False):
breadcrumbs = generate_breadcrumbs_from_path() breadcrumbs = generate_breadcrumbs_from_path()
files = generate_directory(request.user, directory) files = generate_directory(request.user, directory)
form = DirectoryEditForm()
return render( return render(
request, request,
@@ -56,14 +56,18 @@ def comic_list(request, directory_selector=False):
"menu": Menu(request.user, "Browse"), "menu": Menu(request.user, "Browse"),
"title": title, "title": title,
"files": files, "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 @login_required
def perform_action(request, operation, item_type, selector): 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) return HttpResponse(400)
elif operation == 'mark_previous' and item_type == 'Directory': elif operation == 'mark_previous' and item_type == 'Directory':
return HttpResponse(422) return HttpResponse(422)
@@ -74,20 +78,29 @@ def perform_action(request, operation, item_type, selector):
for book in ComicBook.objects.filter(directory__isnull=True): for book in ComicBook.objects.filter(directory__isnull=True):
getattr(book, operation)(request.user) getattr(book, operation)(request.user)
return HttpResponse(204) 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': if item_type == 'ComicBook':
book = get_object_or_404(ComicBook, selector=selector_uuid) book = get_object_or_404(ComicBook, selector=selector_uuid)
getattr(book, operation)(request.user) getattr(book, operation)(request.user)
return HttpResponse(204) return HttpResponse(204)
elif item_type == 'Directory': elif item_type == 'Directory':
directory = get_object_or_404(Directory, selector=selector_uuid) directory = get_object_or_404(Directory, selector=selector_uuid)
if operation == 'set_classification':
getattr(directory, operation)(form.cleaned_data)
else:
getattr(directory, operation)(request.user) getattr(directory, operation)(request.user)
return HttpResponse(204) return HttpResponse(204)
@login_required @login_required
def recent_comics(request): def recent_comics(request):
feed_id, _ = UserMisc.objects.get_or_create(user=request.user)
return render( return render(
request, request,
"comic/recent_comics.html", "comic/recent_comics.html",
@@ -95,7 +108,7 @@ def recent_comics(request):
"breadcrumbs": generate_breadcrumbs_from_menu([("Recent", "/comic/recent/")]), "breadcrumbs": generate_breadcrumbs_from_menu([("Recent", "/comic/recent/")]),
"menu": Menu(request.user, "Recent"), "menu": Menu(request.user, "Recent"),
"title": "Recent Comics", "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) @user_passes_test(lambda u: u.is_superuser)
def users_page(request): def users_page(request):
users = User.objects.all() users = User.objects.all().select_related('usermisc')
crumbs = [("Users", "/comic/settings/users/")] crumbs = [("Users", "/comic/settings/users/")]
context = { context = {
"users": users, "users": users,
@@ -221,6 +234,8 @@ def user_config_page(request, user_id):
if form.cleaned_data["email"] != user.email: if form.cleaned_data["email"] != user.email:
user.email = form.cleaned_data["email"] user.email = form.cleaned_data["email"]
success_message.append("Email Updated.</br>") success_message.append("Email Updated.</br>")
user.usermisc.allowed_to_read = form.cleaned_data['allowed_to_read']
user.usermisc.save()
user.save() user.save()
else: else:
form = EditUserForm(initial=EditUserForm.get_initial_values(user)) 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 = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"])
user.set_password(form.cleaned_data["password"]) user.set_password(form.cleaned_data["password"])
user.save() user.save()
UserMisc.objects.create(user=user)
success_message = "User {} created.".format(user.username) success_message = "User {} created.".format(user.username)
else: else:
@@ -269,14 +283,10 @@ def user_add_page(request):
def read_comic(request, comic_selector): def read_comic(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(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) 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) pages = ComicPage.objects.filter(Comic=book)
@@ -318,33 +328,42 @@ def set_read_page(request, comic_selector, page):
@xframe_options_sameorigin @xframe_options_sameorigin
@login_required @login_required
def get_image(_, comic_selector, page): def get_image(request, comic_selector, page):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector)) selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=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)) img, content = book.get_image(int(page))
return FileResponse(img, content_type=content) return FileResponse(img, content_type=content)
@xframe_options_sameorigin @xframe_options_sameorigin
@login_required @login_required
def comic_thumbnail(_, comic_selector): def comic_thumbnail(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector)) selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=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()) return redirect(book.get_thumbnail_url())
@xframe_options_sameorigin @xframe_options_sameorigin
@login_required @login_required
def directory_thumbnail(_, directory_selector): def directory_thumbnail(request, directory_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector)) selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
folder = Directory.objects.get(selector=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()) return redirect(folder.get_thumbnail_url())
@login_required @login_required
def get_pdf(_, comic_selector): def get_pdf(request, comic_selector):
selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector)) selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector))
book = ComicBook.objects.get(selector=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') 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] [package.dependencies]
django = "*" 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]] [[package]]
name = "django-bootstrap4" name = "django-bootstrap4"
version = "3.0.0" version = "3.0.0"
@@ -443,6 +455,14 @@ category = "dev"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.4" 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)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
brotli = ["brotlipy (>=0.6.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]] [[package]]
name = "win32-setctime" name = "win32-setctime"
version = "1.0.3" version = "1.0.3"
@@ -470,7 +501,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "71642aa577156d70c6033dbc260a2ab03d247a17d9b0b0500a9c9a0e0228fd68" content-hash = "8364532c96609a5598f24f5e77f2b647763fafc8052c1d466dabc154a90c6d09"
[metadata.files] [metadata.files]
asgiref = [ asgiref = [
@@ -564,6 +595,10 @@ django-appconf = [
{file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"}, {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"}, {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 = [ django-bootstrap4 = [
{file = "django-bootstrap4-3.0.0.tar.gz", hash = "sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b"}, {file = "django-bootstrap4-3.0.0.tar.gz", hash = "sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b"},
{file = "django_bootstrap4-3.0.0-py3-none-any.whl", hash = "sha256:76a52fb22a8d3dbb2f7609b21908ce863e941a4462be079bf1d12025e551af37"}, {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-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, {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 = [ urllib3 = [
{file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
{file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, {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 = [ win32-setctime = [
{file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"}, {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"},
{file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, {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" PyMuPDF = "^1.18.12"
django-bootstrap4 = "^3.0.0" django-bootstrap4 = "^3.0.0"
django-csp = "^3.7" django-csp = "^3.7"
django-boost = "^1.7.2"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = "^0.812" mypy = "^0.812"

View File

@@ -76,6 +76,9 @@ dj-database-url==0.5.0 \
django-appconf==1.0.4 \ django-appconf==1.0.4 \
--hash=sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380 \ --hash=sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380 \
--hash=sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06 --hash=sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06
django-boost==1.7.2 \
--hash=sha256:b2460f8613920cdb309cb5c27a0a18d8fb83812f6019c6bb2218f82b33a67ea3 \
--hash=sha256:ca80641314f75446ba815ed9632c64ede025c72bc18ec59af89abce97769a65f
django-bootstrap4==3.0.0; python_version >= "3.6" \ django-bootstrap4==3.0.0; python_version >= "3.6" \
--hash=sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b \ --hash=sha256:bffc96f65386fbd49cae1474393e01d4b414c12fcab0fff50545e6142e7ba19b \
--hash=sha256:76a52fb22a8d3dbb2f7609b21908ce863e941a4462be079bf1d12025e551af37 --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" \ 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:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
--hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f --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" \ 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:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df \
--hash=sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937 --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" \ win32-setctime==1.0.3; sys_platform == "win32" and python_version >= "3.5" \
--hash=sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e \ --hash=sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e \
--hash=sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b --hash=sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b

View File

@@ -5,10 +5,17 @@
.card_list_card { .card_list_card {
width: 200px; width: 200px;
} }
.card .card-badge { .card .unread-badge {
position:absolute; position:absolute;
top:10px; top:10px;
left:10px; left:10px;
padding:5px; padding:5px;
color:white; 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,15 +1,18 @@
var qsRegex; let qsRegex;
var buttonFilter; let buttonFilter;
var $grid = $('.comic-container').isotope({ const js_urls = JSON.parse(document.getElementById('js_urls').textContent)
let $grid = $('.comic-container').isotope({
itemSelector: '.grid-item', itemSelector: '.grid-item',
layoutMode: 'fitRows', layoutMode: 'fitRows',
filter: function() { filter: function() {
var $this = $(this); let $this = $(this);
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true; let searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
var buttonResult = buttonFilter ? $this.is( buttonFilter ) : true; let buttonResult = buttonFilter ? $this.is( buttonFilter ) : true;
return searchResult && buttonResult; return searchResult && buttonResult;
} }
}); });
$('#filters').on( 'click', 'button', function() { $('#filters').on( 'click', 'button', function() {
if (typeof $( this ).attr('data-filter') === "undefined") { if (typeof $( this ).attr('data-filter') === "undefined") {
@@ -20,7 +23,7 @@ var buttonFilter;
} }
}); });
var $quicksearch = $('#quicksearch').keyup( debounce( function() { let $quicksearch = $('#quicksearch').keyup( debounce( function() {
qsRegex = new RegExp($quicksearch.val(), 'gi'); qsRegex = new RegExp($quicksearch.val(), 'gi');
sessionStorage.setItem(window.location.href+'text', $quicksearch.val()); sessionStorage.setItem(window.location.href+'text', $quicksearch.val());
$grid.isotope(); $grid.isotope();
@@ -84,3 +87,49 @@ comic_action_elements.forEach(el => el.addEventListener('click', event => {
let action = target.attr('comic_action') let action = target.attr('comic_action')
comic_action(selector, item_type, 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")}})});