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 %}
-
+
{% if file.item_type == 'Directory' %} {% elif file.item_type == 'ComicBook' %} @@ -63,7 +64,7 @@

-
+
@@ -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 %} - + {% 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