From ce38340a224bef73c1b5245db73a4b7e92ea5653 Mon Sep 17 00:00:00 2001
From: Ajurna
Date: Tue, 11 May 2021 15:52:56 +0100
Subject: [PATCH] Classification (#32)
* added some code cleanup for views.py
* added some code cleanup for views.py
* fixed comics not working in the base directory.
---
cbreader/settings/base.py | 1 +
comic/admin.py | 2 +-
comic/forms.py | 23 ++--
comic/migrations/0025_auto_20210506_1342.py | 23 ++++
comic/migrations/0026_alter_usermisc_user.py | 22 ++++
comic/migrations/0027_auto_20210506_1356.py | 26 ++++
comic/models.py | 44 ++++---
comic/templates/comic/comic_list.html | 42 +++++--
comic/templates/comic/users_page.html | 2 +
comic/templatetags/__init__.py | 0
comic/templatetags/comic_tags.py | 4 +
comic/urls.py | 2 +-
comic/util.py | 13 +-
comic/views.py | 65 ++++++----
poetry.lock | 45 ++++++-
pyproject.toml | 1 +
requirements.txt | 9 ++
static/css/base.css | 9 +-
static/css/base.min.css | 2 +-
static/js/comic_list.js | 123 +++++++++++++------
static/js/comic_list.min.js | 2 +-
21 files changed, 355 insertions(+), 105 deletions(-)
create mode 100644 comic/migrations/0025_auto_20210506_1342.py
create mode 100644 comic/migrations/0026_alter_usermisc_user.py
create mode 100644 comic/migrations/0027_auto_20210506_1356.py
create mode 100644 comic/templatetags/__init__.py
create mode 100644 comic/templatetags/comic_tags.py
diff --git a/cbreader/settings/base.py b/cbreader/settings/base.py
index c7ef1da..16f3344 100644
--- a/cbreader/settings/base.py
+++ b/cbreader/settings/base.py
@@ -40,6 +40,7 @@ INSTALLED_APPS = (
"comic_auth",
'django_extensions',
'imagekit',
+ 'django_boost',
)
MIDDLEWARE = [
diff --git a/comic/admin.py b/comic/admin.py
index 5c2d7e9..4e6a358 100644
--- a/comic/admin.py
+++ b/comic/admin.py
@@ -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',)
\ No newline at end of file
diff --git a/comic/forms.py b/comic/forms.py
index 4f0759c..ea5a57e 100644
--- a/comic/forms.py
+++ b/comic/forms.py
@@ -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
\ No newline at end of file
+ return data
+
+
+class DirectoryEditForm(forms.Form):
+ classification = forms.ChoiceField(choices=Directory.Classification.choices)
diff --git a/comic/migrations/0025_auto_20210506_1342.py b/comic/migrations/0025_auto_20210506_1342.py
new file mode 100644
index 0000000..1167553
--- /dev/null
+++ b/comic/migrations/0025_auto_20210506_1342.py
@@ -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),
+ ),
+ ]
diff --git a/comic/migrations/0026_alter_usermisc_user.py b/comic/migrations/0026_alter_usermisc_user.py
new file mode 100644
index 0000000..a7a5d68
--- /dev/null
+++ b/comic/migrations/0026_alter_usermisc_user.py
@@ -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),
+ ),
+ ]
diff --git a/comic/migrations/0027_auto_20210506_1356.py b/comic/migrations/0027_auto_20210506_1356.py
new file mode 100644
index 0000000..708b7c1
--- /dev/null
+++ b/comic/migrations/0027_auto_20210506_1356.py
@@ -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),
+ ),
+ ]
diff --git a/comic/models.py b/comic/models.py
index f8da574..a2adcfa 100644
--- a/comic/models.py
+++ b/comic/models.py
@@ -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)
diff --git a/comic/templates/comic/comic_list.html b/comic/templates/comic/comic_list.html
index 6ad40cd..185a549 100644
--- a/comic/templates/comic/comic_list.html
+++ b/comic/templates/comic/comic_list.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% load bootstrap4 %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
@@ -48,7 +49,7 @@
{% endif %}
-
+
@@ -75,25 +76,48 @@
{% if file.item_type != 'Directory' %}
+ {% else %}
+
{% endif %}
-{# #}
+
- {% if file.total_unread and file.item_type == 'Directory' %}
- {{ file.total_unread }}
- {% endif %}
-
+ {% if file.total_unread and file.item_type == 'Directory' %}
+ {{ file.total_unread }}
+ {% endif %}
+ {{ file.obj.get_classification_display }}
+
{% endfor %}
-
+
+
+
+
+
+ {% csrf_token %}
+ {% bootstrap_form form %}
+
+
+
+
+
{% endblock %}
{% block script %}
-
+ {{ js_urls|json_script:'js_urls' }}
+
{% endblock %}
\ No newline at end of file
diff --git a/comic/templates/comic/users_page.html b/comic/templates/comic/users_page.html
index 8697472..7fc093d 100644
--- a/comic/templates/comic/users_page.html
+++ b/comic/templates/comic/users_page.html
@@ -11,6 +11,7 @@
Username |
Email |
Superuser |
+ Classification |
@@ -20,6 +21,7 @@
{{user.username}} |
{{user.email}} |
{{user.is_superuser}} |
+ {{ user.usermisc.get_allowed_to_read_display }} |
{% endfor %}
diff --git a/comic/templatetags/__init__.py b/comic/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/comic/templatetags/comic_tags.py b/comic/templatetags/comic_tags.py
new file mode 100644
index 0000000..d0b4881
--- /dev/null
+++ b/comic/templatetags/comic_tags.py
@@ -0,0 +1,4 @@
+from django import template
+
+register = template.Library()
+
diff --git a/comic/urls.py b/comic/urls.py
index a854bfd..1e90cd4 100644
--- a/comic/urls.py
+++ b/comic/urls.py
@@ -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//", feeds.RecentComics()),
+ path("feed//", feeds.RecentComics()),
path("/", views.comic_list, name="comic_list"),
path("/thumb", views.directory_thumbnail, name="directory_thumbnail"),
path("action////", views.perform_action, name="perform_action")
diff --git a/comic/util.py b/comic/util.py
index c0da52a..2ee9ce2 100644
--- a/comic/util.py
+++ b/comic/util.py
@@ -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"]:
diff --git a/comic/views.py b/comic/views.py
index d64ba50..c2ca6e2 100644
--- a/comic/views.py
+++ b/comic/views.py
@@ -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.")
+ 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')
diff --git a/poetry.lock b/poetry.lock
index 0b5fccf..e61d0fa 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -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"},
diff --git a/pyproject.toml b/pyproject.toml
index cbc58ec..64d0d9e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"
diff --git a/requirements.txt b/requirements.txt
index 2209cfc..9e8ae98 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/static/css/base.css b/static/css/base.css
index 10bee81..0eac012 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -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;
}
\ No newline at end of file
diff --git a/static/css/base.min.css b/static/css/base.min.css
index 1f052ac..3458dc6 100644
--- a/static/css/base.min.css
+++ b/static/css/base.min.css
@@ -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}
\ No newline at end of file
+#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}
\ No newline at end of file
diff --git a/static/js/comic_list.js b/static/js/comic_list.js
index 9dfac8b..7e00fc5 100644
--- a/static/js/comic_list.js
+++ b/static/js/comic_list.js
@@ -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')
+ },
+ })
+
+
+})
\ No newline at end of file
diff --git a/static/js/comic_list.min.js b/static/js/comic_list.min.js
index d3b780e..69ce3b7 100644
--- a/static/js/comic_list.min.js
+++ b/static/js/comic_list.min.js
@@ -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)}));
\ No newline at end of file
+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")}})});
\ No newline at end of file