Merge pull request #5 from apoclyps/black-formatting

[ISSUE-4] Applying black formatting
This commit is contained in:
2019-07-26 08:45:41 +01:00
committed by GitHub
37 changed files with 990 additions and 1150 deletions

View File

@@ -22,10 +22,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '=3tf-@u1t7x4%$yr++59+8tspl4ao&r3&!bb6l(t&$#6@bfkwg' SECRET_KEY = "=3tf-@u1t7x4%$yr++59+8tspl4ao&r3&!bb6l(t&$#6@bfkwg"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', True) DEBUG = os.environ.get("DJANGO_DEBUG", True)
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",") ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")
@@ -33,46 +33,46 @@ ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'snowpenguin.django.recaptcha2', "snowpenguin.django.recaptcha2",
'comic', "comic",
'comic_auth', "comic_auth",
) )
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'cbreader.urls' ROOT_URLCONF = "cbreader.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': ['templates'], "DIRS": ["templates"],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ]
},
}, },
}
] ]
WSGI_APPLICATION = 'cbreader.wsgi.application' WSGI_APPLICATION = "cbreader.wsgi.application"
# Database # Database
@@ -83,20 +83,15 @@ DATABASE_URL = os.getenv("DATABASE_URL")
if DATABASE_URL: if DATABASE_URL:
DATABASES = {"default": dj_database_url.config(conn_max_age=500)} DATABASES = {"default": dj_database_url.config(conn_max_age=500)}
else: else:
DATABASES = { DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3")}}
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/ # https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en-ie' LANGUAGE_CODE = "en-ie"
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
@@ -108,16 +103,16 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/ # https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
LOGIN_REDIRECT_URL = '/comic/' LOGIN_REDIRECT_URL = "/comic/"
LOGIN_URL = '/login/' LOGIN_URL = "/login/"
UNRAR_TOOL = os.getenv("UNRAR_TOOL", None) UNRAR_TOOL = os.getenv("UNRAR_TOOL", None)
CBREADER_USE_RECAPTCHA = False CBREADER_USE_RECAPTCHA = False
RECAPTCHA_PRIVATE_KEY = '' RECAPTCHA_PRIVATE_KEY = ""
RECAPTCHA_PUBLIC_KEY = '' RECAPTCHA_PUBLIC_KEY = ""
COMIC_DIR = "/media/comics" COMIC_DIR = "/media/comics"

View File

@@ -1,123 +0,0 @@
"""
Django settings for cbreader project.
Generated by 'django-admin startproject' using Django 1.8.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import dj_database_url
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '=3tf-@u1t7x4%$yr++59+8tspl4ao&r3&!bb6l(t&$#6@bfkwg'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', True)
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'snowpenguin.django.recaptcha2',
'comic',
'comic_auth',
)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'cbreader.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'cbreader.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
DATABASE_URL = os.getenv("TEST_DATABASE_URL")
if DATABASE_URL:
DATABASES = {"default": dj_database_url.config(conn_max_age=500)}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en-ie'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/'
LOGIN_REDIRECT_URL = '/comic/'
LOGIN_URL = '/login/'
UNRAR_TOOL = os.getenv("UNRAR_TOOL", None)
CBREADER_USE_RECAPTCHA = False
RECAPTCHA_PRIVATE_KEY = ''
RECAPTCHA_PUBLIC_KEY = ''
COMIC_DIR = "/media/comics"

View File

@@ -0,0 +1,118 @@
"""
Django settings for cbreader project.
Generated by 'django-admin startproject' using Django 1.8.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import dj_database_url
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "=3tf-@u1t7x4%$yr++59+8tspl4ao&r3&!bb6l(t&$#6@bfkwg"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DJANGO_DEBUG", True)
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")
# Application definition
INSTALLED_APPS = (
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"snowpenguin.django.recaptcha2",
"comic",
"comic_auth",
)
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "cbreader.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": ["templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}
]
WSGI_APPLICATION = "cbreader.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
DATABASE_URL = os.getenv("TEST_DATABASE_URL")
if DATABASE_URL:
DATABASES = {"default": dj_database_url.config(conn_max_age=500)}
else:
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3")}}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = "en-ie"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = "/static/"
LOGIN_REDIRECT_URL = "/comic/"
LOGIN_URL = "/login/"
UNRAR_TOOL = os.getenv("UNRAR_TOOL", None)
CBREADER_USE_RECAPTCHA = False
RECAPTCHA_PRIVATE_KEY = ""
RECAPTCHA_PUBLIC_KEY = ""
COMIC_DIR = "/media/comics"

View File

@@ -20,11 +20,11 @@ import comic.views
import comic_auth.views import comic_auth.views
urlpatterns = [ urlpatterns = [
url(r'^$', comic.views.comic_redirect), url(r"^$", comic.views.comic_redirect),
url(r'^login/', comic_auth.views.comic_login), url(r"^login/", comic_auth.views.comic_login),
url(r'^logout/', comic_auth.views.comic_logout), url(r"^logout/", comic_auth.views.comic_logout),
url(r'^setup/', comic.views.initial_setup), url(r"^setup/", comic.views.initial_setup),
url(r'^comic/', include('comic.urls')), url(r"^comic/", include("comic.urls")),
url(r'^admin/', admin.site.urls), url(r"^admin/", admin.site.urls),
# url(r'^silk/', include('silk.urls', namespace='silk')) # url(r'^silk/', include('silk.urls', namespace='silk'))
] ]

View File

@@ -5,24 +5,24 @@ from comic.models import Setting, ComicBook, ComicPage, ComicStatus, Directory
@admin.register(Setting) @admin.register(Setting)
class SettingAdmin(admin.ModelAdmin): class SettingAdmin(admin.ModelAdmin):
list_display = ('name', 'value') list_display = ("name", "value")
@admin.register(ComicBook) @admin.register(ComicBook)
class ComicBookAdmin(admin.ModelAdmin): class ComicBookAdmin(admin.ModelAdmin):
list_display = ['file_name', 'date_added'] list_display = ["file_name", "date_added"]
search_fields = ['file_name'] search_fields = ["file_name"]
@admin.register(ComicPage) @admin.register(ComicPage)
class ComicPageAdmin(admin.ModelAdmin): class ComicPageAdmin(admin.ModelAdmin):
list_display = ('Comic', 'index', 'page_file_name', 'content_type') list_display = ("Comic", "index", "page_file_name", "content_type")
list_filter = ['Comic'] list_filter = ["Comic"]
@admin.register(ComicStatus) @admin.register(ComicStatus)
class ComicStatusAdmin(admin.ModelAdmin): class ComicStatusAdmin(admin.ModelAdmin):
list_display = ['user', 'comic', 'last_read_page', 'unread'] list_display = ["user", "comic", "last_read_page", "unread"]
@admin.register(Directory) @admin.register(Directory)

View File

@@ -19,14 +19,14 @@ class RecentComics(Feed):
@staticmethod @staticmethod
def items() -> ComicBook: def items() -> ComicBook:
return ComicBook.objects.order_by('-date_added')[:10] return ComicBook.objects.order_by("-date_added")[:10]
def item_title(self, item: ComicBook) -> str: def item_title(self, item: ComicBook) -> str:
return item.file_name return item.file_name
def item_description(self, item: ComicBook) -> str: def item_description(self, item: ComicBook) -> str:
return item.date_added.strftime('%a, %e %b %Y %H:%M') return item.date_added.strftime("%a, %e %b %Y %H:%M")
# item_link is only needed if NewsItem has no get_absolute_url method. # item_link is only needed if NewsItem has no get_absolute_url method.
def item_link(self, item: ComicBook) -> str: def item_link(self, item: ComicBook) -> str:
return '/comic/read/{0}/0/'.format(urlsafe_base64_encode(item.selector.bytes)) return "/comic/read/{0}/0/".format(urlsafe_base64_encode(item.selector.bytes))

View File

@@ -7,214 +7,137 @@ from comic.models import Setting
class InitialSetupForm(forms.Form): class InitialSetupForm(forms.Form):
username = forms.CharField(help_text='Username', username = forms.CharField(help_text="Username", widget=forms.TextInput(attrs={"class": "form-control"}))
widget=forms.TextInput( email = forms.CharField(help_text="Email Address", widget=forms.TextInput(attrs={"class": "form-control"}))
attrs={ password = forms.CharField(help_text="New Password", widget=forms.PasswordInput(attrs={"class": "form-control"}))
'class': 'form-control', password_confirm = forms.CharField(
} help_text="New Password Confirmation", widget=forms.PasswordInput(attrs={"class": "form-control"})
)) )
email = forms.CharField(help_text='Email Address', base_dir = forms.CharField(help_text="Base Directory", widget=forms.TextInput(attrs={"class": "form-control"}))
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
password = forms.CharField(help_text='New Password',
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
password_confirm = forms.CharField(help_text='New Password Confirmation',
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
base_dir = forms.CharField(help_text='Base Directory',
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
def clean_base_dir(self): def clean_base_dir(self):
data = self.cleaned_data['base_dir'] data = self.cleaned_data["base_dir"]
if not path.isdir(data): if not path.isdir(data):
raise forms.ValidationError('This is not a valid Directory') raise forms.ValidationError("This is not a valid Directory")
return data return data
def clean(self): def clean(self):
form_data = self.cleaned_data form_data = self.cleaned_data
if form_data['password'] != form_data['password_confirm']: if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError('Passwords do not match.') raise forms.ValidationError("Passwords do not match.")
if len(form_data['password']) < 8: if len(form_data["password"]) < 8:
raise forms.ValidationError('Password is too short') raise forms.ValidationError("Password is too short")
return form_data return form_data
class AccountForm(forms.Form): class AccountForm(forms.Form):
username = forms.CharField(help_text='Username', username = forms.CharField(
help_text="Username",
required=False, required=False,
widget=forms.TextInput( widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
attrs={ )
'class': 'form-control disabled', email = forms.CharField(help_text="Email Address", widget=forms.TextInput(attrs={"class": "form-control"}))
'readonly': True, password = forms.CharField(
} help_text="New Password", required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
)) )
email = forms.CharField(help_text='Email Address', password_confirm = forms.CharField(
widget=forms.TextInput( help_text="New Password Confirmation",
attrs={
'class': 'form-control'
}
))
password = forms.CharField(help_text='New Password',
required=False, required=False,
widget=forms.PasswordInput( widget=forms.PasswordInput(attrs={"class": "form-control"}),
attrs={ )
'class': 'form-control',
}
))
password_confirm = forms.CharField(help_text='New Password Confirmation',
required=False,
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
def clean_email(self): def clean_email(self):
data = self.cleaned_data['email'] data = self.cleaned_data["email"]
user = User.objects.get(username=self.cleaned_data['username']) user = User.objects.get(username=self.cleaned_data["username"])
if data == user.email: if data == user.email:
return data return data
if User.objects.filter(email=data).exists(): if User.objects.filter(email=data).exists():
raise forms.ValidationError('Email Address is in use') raise forms.ValidationError("Email Address is in use")
return data return data
def clean(self): def clean(self):
form_data = self.cleaned_data form_data = self.cleaned_data
if form_data['password'] != form_data['password_confirm']: if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError('Passwords do not match.') raise forms.ValidationError("Passwords do not match.")
if len(form_data['password']) < 8 & len(form_data['password']) != 0: if len(form_data["password"]) < 8 & len(form_data["password"]) != 0:
raise forms.ValidationError('Password is too short') raise forms.ValidationError("Password is too short")
return form_data return form_data
class AddUserForm(forms.Form): class AddUserForm(forms.Form):
username = forms.CharField(help_text='Username', username = forms.CharField(help_text="Username", widget=forms.TextInput(attrs={"class": "form-control"}))
widget=forms.TextInput( email = forms.CharField(help_text="Email Address", widget=forms.TextInput(attrs={"class": "form-control"}))
attrs={ password = forms.CharField(help_text="New Password", widget=forms.PasswordInput(attrs={"class": "form-control"}))
'class': 'form-control', password_confirm = forms.CharField(
} help_text="New Password Confirmation", widget=forms.PasswordInput(attrs={"class": "form-control"})
)) )
email = forms.CharField(help_text='Email Address',
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
password = forms.CharField(help_text='New Password',
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
password_confirm = forms.CharField(help_text='New Password Confirmation',
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
def clean_username(self): def clean_username(self):
data = self.cleaned_data['username'] data = self.cleaned_data["username"]
if User.objects.filter(username=data).exists(): if User.objects.filter(username=data).exists():
raise forms.ValidationError('This username Exists.') raise forms.ValidationError("This username Exists.")
return data return data
def clean_email(self): def clean_email(self):
data = self.cleaned_data['email'] data = self.cleaned_data["email"]
if User.objects.filter(email=data).exists(): if User.objects.filter(email=data).exists():
raise forms.ValidationError('Email Address is in use') raise forms.ValidationError("Email Address is in use")
return data return data
def clean(self): def clean(self):
form_data = self.cleaned_data form_data = self.cleaned_data
if form_data['password'] != form_data['password_confirm']: if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError('Passwords do not match.') raise forms.ValidationError("Passwords do not match.")
if len(form_data['password']) < 8: if len(form_data["password"]) < 8:
raise forms.ValidationError('Password is too short') raise forms.ValidationError("Password is too short")
return form_data return form_data
class EditUserForm(forms.Form): class EditUserForm(forms.Form):
username = forms.CharField(help_text='Username', username = forms.CharField(
help_text="Username",
required=False, required=False,
widget=forms.TextInput( widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
attrs={ )
'class': 'form-control disabled', email = forms.CharField(help_text="Email Address", widget=forms.TextInput(attrs={"class": "form-control"}))
'readonly': True, password = forms.CharField(
} help_text="New Password", required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
)) )
email = forms.CharField(help_text='Email Address',
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
password = forms.CharField(help_text='New Password',
required=False,
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
# TODO: allow setting superuser on users # TODO: allow setting superuser on users
@staticmethod @staticmethod
def get_initial_values(user): def get_initial_values(user):
out = { out = {"username": user.username, "email": user.email}
'username': user.username,
'email': user.email
}
return out return out
def clean_email(self): def clean_email(self):
data = self.cleaned_data['email'] data = self.cleaned_data["email"]
user = User.objects.get(username=self.cleaned_data['username']) user = User.objects.get(username=self.cleaned_data["username"])
if data == user.email: if data == user.email:
return data return data
if User.objects.filter(email=data).exists(): if User.objects.filter(email=data).exists():
raise forms.ValidationError('Email Address is in use') raise forms.ValidationError("Email Address is in use")
return data 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 SettingsForm(forms.Form): class SettingsForm(forms.Form):
base_dir = forms.CharField(help_text='Base Directory', base_dir = forms.CharField(help_text="Base Directory", widget=forms.TextInput(attrs={"class": "form-control"}))
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
def clean_base_dir(self): def clean_base_dir(self):
data = self.cleaned_data['base_dir'] data = self.cleaned_data["base_dir"]
if not path.isdir(data): if not path.isdir(data):
raise forms.ValidationError('This is not a valid Directory') raise forms.ValidationError("This is not a valid Directory")
return data return data
@staticmethod @staticmethod
def get_initial_values(): def get_initial_values():
base_dir, _ = Setting.objects.get_or_create(name='BASE_DIR') base_dir, _ = Setting.objects.get_or_create(name="BASE_DIR")
initial = { initial = {"base_dir": base_dir.value}
'base_dir': base_dir.value,
}
return initial return initial

View File

@@ -7,11 +7,11 @@ from comic.models import Setting, Directory, ComicBook
class Command(BaseCommand): class Command(BaseCommand):
help = 'Scan directories to Update Comic DB' help = "Scan directories to Update Comic DB"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.base_dir = Setting.objects.get(name='BASE_DIR').value self.base_dir = Setting.objects.get(name="BASE_DIR").value
def handle(self, *args, **options): def handle(self, *args, **options):
self.scan_directory() self.scan_directory()
@@ -36,25 +36,21 @@ class Command(BaseCommand):
for file in os.listdir(comic_dir): for file in os.listdir(comic_dir):
if isdir(os.path.join(comic_dir, file)): if isdir(os.path.join(comic_dir, file)):
if directory: if directory:
next_directory, created = Directory.objects.get_or_create(name=file, next_directory, created = Directory.objects.get_or_create(name=file, parent=directory)
parent=directory)
else: else:
next_directory, created = Directory.objects.get_or_create(name=file, next_directory, created = Directory.objects.get_or_create(name=file, parent__isnull=True)
parent__isnull=True)
if created: if created:
next_directory.save() next_directory.save()
self.scan_directory(next_directory) self.scan_directory(next_directory)
else: else:
try: try:
if directory: if directory:
book = ComicBook.objects.get(file_name=file, book = ComicBook.objects.get(file_name=file, directory=directory)
directory=directory)
if book.version == 0: if book.version == 0:
book.version = 1 book.version = 1
book.save() book.save()
else: else:
book = ComicBook.objects.get(file_name=file, book = ComicBook.objects.get(file_name=file, directory__isnull=True)
directory__isnull=True)
if book.version == 0: if book.version == 0:
if directory: if directory:
book.directory = directory book.directory = directory

View File

@@ -6,16 +6,15 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Setting', name="Setting",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=50)), ("name", models.CharField(max_length=50)),
('value', models.TextField()), ("value", models.TextField()),
], ],
), )
] ]

View File

@@ -6,14 +6,8 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0001_initial")]
('comic', '0001_initial'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(model_name="setting", name="name", field=models.CharField(unique=True, max_length=50))
model_name='setting',
name='name',
field=models.CharField(unique=True, max_length=50),
),
] ]

View File

@@ -6,27 +6,25 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0002_auto_20150616_1613")]
('comic', '0002_auto_20150616_1613'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ComicBook', name="ComicBook",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('file_name', models.CharField(unique=True, max_length=100)), ("file_name", models.CharField(unique=True, max_length=100)),
('last_read_page', models.IntegerField()), ("last_read_page", models.IntegerField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='ComicPage', name="ComicPage",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('index', models.IntegerField()), ("index", models.IntegerField()),
('page_file_name', models.CharField(max_length=100)), ("page_file_name", models.CharField(max_length=100)),
('content_type', models.CharField(max_length=30)), ("content_type", models.CharField(max_length=30)),
('Comic', models.ForeignKey(to='comic.ComicBook', on_delete=models.CASCADE)), ("Comic", models.ForeignKey(to="comic.ComicBook", on_delete=models.CASCADE)),
], ],
), ),
] ]

View File

@@ -6,15 +6,10 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0003_comicbook_comicpage")]
('comic', '0003_comicbook_comicpage'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='comicbook', model_name="comicbook", name="unread", field=models.BooleanField(default=True), preserve_default=False
name='unread', )
field=models.BooleanField(default=True),
preserve_default=False,
),
] ]

View File

@@ -7,36 +7,27 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("comic", "0004_comicbook_unread")]
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comic', '0004_comicbook_unread'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ComicStatus', name="ComicStatus",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('last_read_page', models.IntegerField()), ("last_read_page", models.IntegerField()),
('unread', models.BooleanField()), ("unread", models.BooleanField()),
], ],
), ),
migrations.RemoveField( migrations.RemoveField(model_name="comicbook", name="last_read_page"),
model_name='comicbook', migrations.RemoveField(model_name="comicbook", name="unread"),
name='last_read_page', migrations.AddField(
), model_name="comicstatus",
migrations.RemoveField( name="comic",
model_name='comicbook', field=models.ForeignKey(to="comic.ComicBook", on_delete=models.CASCADE),
name='unread',
), ),
migrations.AddField( migrations.AddField(
model_name='comicstatus', model_name="comicstatus",
name='comic', name="user",
field=models.ForeignKey(to='comic.ComicBook', on_delete=models.CASCADE),
),
migrations.AddField(
model_name='comicstatus',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
), ),
] ]

View File

@@ -6,19 +6,9 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0005_auto_20150625_1400")]
('comic', '0005_auto_20150625_1400'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(model_name="comicstatus", name="last_read_page", field=models.IntegerField(default=0)),
model_name='comicstatus', migrations.AlterField(model_name="comicstatus", name="unread", field=models.BooleanField(default=True)),
name='last_read_page',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='comicstatus',
name='unread',
field=models.BooleanField(default=True),
),
] ]

View File

@@ -6,14 +6,8 @@ from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0006_auto_20150625_1411")]
('comic', '0006_auto_20150625_1411'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(model_name="setting", name="name", field=models.CharField(unique=True, max_length=100))
model_name='setting',
name='name',
field=models.CharField(unique=True, max_length=100),
),
] ]

View File

@@ -11,39 +11,40 @@ import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0007_auto_20150626_1820")]
('comic', '0007_auto_20150626_1820'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Directory', name="Directory",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('name', models.CharField(max_length=100)), ("name", models.CharField(max_length=100)),
('selector', models.UUIDField(default=uuid.uuid4, null=True)), ("selector", models.UUIDField(default=uuid.uuid4, null=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comic.Directory')), (
"parent",
models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="comic.Directory"
),
),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='comicbook', model_name="comicbook",
name='date_added', name="date_added",
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 31, 10, 40, 30, 62170, tzinfo=utc)), field=models.DateTimeField(
auto_now_add=True, default=datetime.datetime(2016, 3, 31, 10, 40, 30, 62170, tzinfo=utc)
),
preserve_default=False, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='comicbook', model_name="comicbook", name="selector", field=models.UUIDField(default=uuid.uuid4, null=True)
name='selector',
field=models.UUIDField(default=uuid.uuid4, null=True),
), ),
migrations.AddField(model_name="comicbook", name="version", field=models.IntegerField(default=0)),
migrations.AddField( migrations.AddField(
model_name='comicbook', model_name="comicbook",
name='version', name="directory",
field=models.IntegerField(default=0), field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="comic.Directory"
), ),
migrations.AddField(
model_name='comicbook',
name='directory',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comic.Directory'),
), ),
] ]

View File

@@ -8,11 +8,11 @@ import uuid
def gen_uuid(apps, schema_editor): def gen_uuid(apps, schema_editor):
comicbook = apps.get_model('comic', 'comicbook') comicbook = apps.get_model("comic", "comicbook")
for row in comicbook.objects.all(): for row in comicbook.objects.all():
row.selector = uuid.uuid4() row.selector = uuid.uuid4()
row.save() row.save()
directory = apps.get_model('comic', 'directory') directory = apps.get_model("comic", "directory")
for row in directory.objects.all(): for row in directory.objects.all():
row.selector = uuid.uuid4() row.selector = uuid.uuid4()
row.save() row.save()
@@ -20,10 +20,6 @@ def gen_uuid(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0008_auto_20160331_1140")]
('comic', '0008_auto_20160331_1140'),
]
operations = [ operations = [migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop)]
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
]

View File

@@ -9,19 +9,13 @@ import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0009_auto_20160331_1140")]
('comic', '0009_auto_20160331_1140'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='comicbook', model_name="comicbook", name="selector", field=models.UUIDField(default=uuid.uuid4, unique=True)
name='selector',
field=models.UUIDField(default=uuid.uuid4, unique=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='directory', model_name="directory", name="selector", field=models.UUIDField(default=uuid.uuid4, unique=True)
name='selector',
field=models.UUIDField(default=uuid.uuid4, unique=True),
), ),
] ]

View File

@@ -7,14 +7,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0010_auto_20160331_1140")]
('comic', '0010_auto_20160331_1140'),
]
operations = [ operations = [migrations.AlterField(model_name="comicbook", name="version", field=models.IntegerField(default=1))]
migrations.AlterField(
model_name='comicbook',
name='version',
field=models.IntegerField(default=1),
),
]

View File

@@ -8,19 +8,17 @@ import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0011_auto_20160331_1141")]
('comic', '0011_auto_20160331_1141'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='comicbook', model_name="comicbook",
name='selector', name="selector",
field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='directory', model_name="directory",
name='selector', name="selector",
field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
), ),
] ]

View File

@@ -7,14 +7,8 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0012_auto_20160401_0949")]
('comic', '0012_auto_20160401_0949'),
]
operations = [ operations = [
migrations.AddField( migrations.AddField(model_name="comicstatus", name="finished", field=models.BooleanField(default=False))
model_name='comicstatus',
name='finished',
field=models.BooleanField(default=False),
),
] ]

View File

@@ -5,22 +5,19 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
from django.db.models import Max from django.db.models import Max
def set_finished(apps, schema_editor): def set_finished(apps, schema_editor):
comicstatus = apps.get_model('comic', 'comicstatus') comicstatus = apps.get_model("comic", "comicstatus")
comicpage = apps.get_model('comic', 'ComicPage') comicpage = apps.get_model("comic", "ComicPage")
for row in comicstatus.objects.all(): for row in comicstatus.objects.all():
last_page = comicpage.objects.filter(Comic=row.comic).aggregate(Max('index')) last_page = comicpage.objects.filter(Comic=row.comic).aggregate(Max("index"))
if row.last_read_page == last_page['index__max']: if row.last_read_page == last_page["index__max"]:
row.finished = True row.finished = True
row.save() row.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0013_comicstatus_finished")]
('comic', '0013_comicstatus_finished'),
]
operations = [ operations = [migrations.RunPython(set_finished, reverse_code=migrations.RunPython.noop)]
migrations.RunPython(set_finished, reverse_code=migrations.RunPython.noop),
]

View File

@@ -7,14 +7,8 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0014_auto_20160404_1402")]
('comic', '0014_auto_20160404_1402'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(model_name="comicbook", name="file_name", field=models.CharField(max_length=100))
model_name='comicbook',
name='file_name',
field=models.CharField(max_length=100),
),
] ]

View File

@@ -6,14 +6,8 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0015_auto_20160405_1126")]
('comic', '0015_auto_20160405_1126'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(model_name="comicpage", name="page_file_name", field=models.CharField(max_length=200))
model_name='comicpage',
name='page_file_name',
field=models.CharField(max_length=200),
),
] ]

View File

@@ -10,19 +10,18 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("comic", "0016_auto_20160414_1335")]
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comic', '0016_auto_20160414_1335'),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='UserMisc', name="UserMisc",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
('feed_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), ("feed_id", models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)),
( (
'user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), "user",
], models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
), ),
],
)
] ]

View File

@@ -6,17 +6,13 @@ from django.db import migrations
def gen_feeds(apps, schema_editor): def gen_feeds(apps, schema_editor):
user_misc = apps.get_model('comic', 'UserMisc') user_misc = apps.get_model("comic", "UserMisc")
User = apps.get_model('auth', 'user') User = apps.get_model("auth", "user")
for user in User.objects.all(): for user in User.objects.all():
um = user_misc.objects.create(user=user) um = user_misc.objects.create(user=user)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [("comic", "0017_usermisc")]
('comic', '0017_usermisc'),
]
operations = [ operations = [migrations.RunPython(gen_feeds, reverse_code=migrations.RunPython.noop)]
migrations.RunPython(gen_feeds, reverse_code=migrations.RunPython.noop),
]

View File

@@ -27,11 +27,11 @@ class Setting(models.Model):
class Directory(models.Model): class Directory(models.Model):
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)
def __str__(self): def __str__(self):
return 'Directory: {0}; {1}'.format(self.name, self.parent) return "Directory: {0}; {1}".format(self.name, self.parent)
@property @property
def path(self): def path(self):
@@ -83,7 +83,7 @@ class ComicBook(models.Model):
return urlsafe_base64_encode(self.selector.bytes) return urlsafe_base64_encode(self.selector.bytes)
def get_image(self, page): def get_image(self, page):
base_dir = Setting.objects.get(name='BASE_DIR').value base_dir = Setting.objects.get(name="BASE_DIR").value
if self.directory: if self.directory:
archive_path = path.join(base_dir, self.directory.path, self.file_name) archive_path = path.join(base_dir, self.directory.path, self.file_name)
else: else:
@@ -109,11 +109,11 @@ class ComicBook(models.Model):
class Navigation: class Navigation:
next_index = 0 next_index = 0
next_path = '' next_path = ""
prev_index = 0 prev_index = 0
prev_path = '' prev_path = ""
cur_index = 0 cur_index = 0
cur_path = '' cur_path = ""
q_prev_to_directory = False q_prev_to_directory = False
q_next_to_directory = False q_next_to_directory = False
@@ -122,10 +122,7 @@ class ComicBook(models.Model):
setattr(self, arg, kwargs[arg]) setattr(self, arg, kwargs[arg])
def nav(self, page, user): def nav(self, page, user):
out = self.Navigation( out = self.Navigation(cur_index=page, cur_path=urlsafe_base64_encode(self.selector.bytes))
cur_index=page,
cur_path=urlsafe_base64_encode(self.selector.bytes)
)
if page == 0: if page == 0:
out.prev_path, out.prev_index = self.nav_get_prev_comic(user) out.prev_path, out.prev_index = self.nav_get_prev_comic(user)
if out.prev_index == -1: if out.prev_index == -1:
@@ -144,7 +141,7 @@ class ComicBook(models.Model):
return out return out
def nav_get_prev_comic(self, user): def nav_get_prev_comic(self, user):
base_dir = Setting.objects.get(name='BASE_DIR').value base_dir = Setting.objects.get(name="BASE_DIR").value
if self.directory: if self.directory:
folder = path.join(base_dir, self.directory.path) folder = path.join(base_dir, self.directory.path)
else: else:
@@ -155,7 +152,7 @@ class ComicBook(models.Model):
if self.directory: if self.directory:
comic_path = urlsafe_base64_encode(self.directory.selector.bytes) comic_path = urlsafe_base64_encode(self.directory.selector.bytes)
else: else:
comic_path = '' comic_path = ""
index = -1 index = -1
else: else:
prev_comic = dir_list[comic_index - 1] prev_comic = dir_list[comic_index - 1]
@@ -163,11 +160,9 @@ class ComicBook(models.Model):
if not path.isdir(path.join(folder, prev_comic)): if not path.isdir(path.join(folder, prev_comic)):
try: try:
if self.directory: if self.directory:
book = ComicBook.objects.get(file_name=prev_comic, book = ComicBook.objects.get(file_name=prev_comic, directory=self.directory)
directory=self.directory)
else: else:
book = ComicBook.objects.get(file_name=prev_comic, book = ComicBook.objects.get(file_name=prev_comic, directory__isnull=True)
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(prev_comic, self.directory)
@@ -180,12 +175,12 @@ class ComicBook(models.Model):
if self.directory: if self.directory:
comic_path = urlsafe_base64_encode(self.directory.selector.bytes) comic_path = urlsafe_base64_encode(self.directory.selector.bytes)
else: else:
comic_path = '' comic_path = ""
index = -1 index = -1
return comic_path, index return comic_path, index
def nav_get_next_comic(self, user): def nav_get_next_comic(self, user):
base_dir = Setting.objects.get(name='BASE_DIR').value base_dir = Setting.objects.get(name="BASE_DIR").value
if self.directory: if self.directory:
folder = path.join(base_dir, self.directory.path) folder = path.join(base_dir, self.directory.path)
else: else:
@@ -196,11 +191,9 @@ class ComicBook(models.Model):
next_comic = dir_list[comic_index + 1] next_comic = dir_list[comic_index + 1]
try: try:
if self.directory: if self.directory:
book = ComicBook.objects.get(file_name=next_comic, book = ComicBook.objects.get(file_name=next_comic, directory=self.directory)
directory=self.directory)
else: else:
book = ComicBook.objects.get(file_name=next_comic, book = ComicBook.objects.get(file_name=next_comic, directory__isnull=True)
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(next_comic, self.directory)
@@ -215,18 +208,18 @@ class ComicBook(models.Model):
if self.directory: if self.directory:
comic_path = urlsafe_base64_encode(self.directory.selector.bytes) comic_path = urlsafe_base64_encode(self.directory.selector.bytes)
else: else:
comic_path = '' comic_path = ""
index = -1 index = -1
return comic_path, index return comic_path, index
class DirFile: class DirFile:
def __init__(self): def __init__(self):
self.name = '' self.name = ""
self.isdir = False self.isdir = False
self.icon = '' self.icon = ""
self.iscb = False self.iscb = False
self.location = '' self.location = ""
self.label = '' self.label = ""
self.cur_page = 0 self.cur_page = 0
def __str__(self): def __str__(self):
@@ -234,7 +227,7 @@ class ComicBook(models.Model):
@property @property
def pages(self): def pages(self):
return [cp for cp in ComicPage.objects.filter(Comic=self).order_by('index')] return [cp for cp in ComicPage.objects.filter(Comic=self).order_by("index")]
def page_name(self, index): def page_name(self, index):
return ComicPage.objects.get(Comic=self, index=index).page_file_name return ComicPage.objects.get(Comic=self, index=index).page_file_name
@@ -247,15 +240,14 @@ class ComicBook(models.Model):
:type directory: Directory :type directory: Directory
""" """
try: try:
book = ComicBook.objects.get(file_name=comic_file_name, book = ComicBook.objects.get(file_name=comic_file_name, version=0)
version=0)
book.directory = directory book.directory = directory
book.version = 1 book.version = 1
book.save() book.save()
return book return book
except ComicBook.DoesNotExist: except ComicBook.DoesNotExist:
pass pass
base_dir = Setting.objects.get(name='BASE_DIR').value base_dir = Setting.objects.get(name="BASE_DIR").value
if directory: if directory:
comic_full_path = path.join(base_dir, directory.get_path(), comic_file_name) comic_full_path = path.join(base_dir, directory.get_path(), comic_file_name)
else: else:
@@ -272,32 +264,30 @@ class ComicBook(models.Model):
return comic_file_name return comic_file_name
with atomic(): with atomic():
if directory: if directory:
book = ComicBook(file_name=comic_file_name, book = ComicBook(file_name=comic_file_name, directory=directory)
directory=directory)
else: else:
book = ComicBook(file_name=comic_file_name) book = ComicBook(file_name=comic_file_name)
book.save() book.save()
page_index = 0 page_index = 0
for page_file_name in sorted([str(x) for x in cbx.namelist()], key=str.lower): for page_file_name in sorted([str(x) for x in cbx.namelist()], key=str.lower):
try: try:
dot_index = page_file_name.rindex('.') + 1 dot_index = page_file_name.rindex(".") + 1
except ValueError: except ValueError:
continue continue
ext = page_file_name.lower()[dot_index:] ext = page_file_name.lower()[dot_index:]
if ext in ['jpg', 'jpeg']: if ext in ["jpg", "jpeg"]:
content_type = 'image/jpeg' content_type = "image/jpeg"
elif ext == 'png': elif ext == "png":
content_type = 'image/png' content_type = "image/png"
elif ext == 'bmp': elif ext == "bmp":
content_type = 'image/bmp' content_type = "image/bmp"
elif ext == 'gif': elif ext == "gif":
content_type = 'image/gif' content_type = "image/gif"
else: else:
content_type = 'text/plain' content_type = "text/plain"
page = ComicPage(Comic=book, page = ComicPage(
index=page_index, Comic=book, index=page_index, page_file_name=page_file_name, content_type=content_type
page_file_name=page_file_name, )
content_type=content_type)
page.save() page.save()
page_index += 1 page_index += 1
return book return book
@@ -336,8 +326,12 @@ class ComicStatus(models.Model):
return self.__repr__() return self.__repr__()
def __repr__(self): def __repr__(self):
return f'<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:' \ return (
f'{self.unread}:{self.finished}' f"<ComicStatus:{self.user.username}:{self.comic.file_name}:{self.last_read_page}:"
f"{self.unread}:{self.finished}"
)
# TODO: add support to reference items last being read # TODO: add support to reference items last being read

View File

@@ -74,10 +74,10 @@ For more details, refer to source.
""" """
__version__ = '2.7' __version__ = "2.7"
# export only interesting items # export only interesting items
__all__ = ['is_rarfile', 'RarInfo', 'RarFile', 'RarExtFile'] __all__ = ["is_rarfile", "RarInfo", "RarFile", "RarExtFile"]
## ##
## Imports and compat - support both Python 2.x and 3.x ## Imports and compat - support both Python 2.x and 3.x
@@ -93,6 +93,7 @@ from datetime import datetime
# only needed for encryped headers # only needed for encryped headers
try: try:
from Crypto.Cipher import AES from Crypto.Cipher import AES
try: try:
from hashlib import sha1 from hashlib import sha1
except ImportError: except ImportError:
@@ -108,6 +109,8 @@ if sys.hexversion < 0x3000000:
# py2.6 has broken bytes() # py2.6 has broken bytes()
def bytes(s, enc): def bytes(s, enc):
return str(s) return str(s)
else: else:
unicode = str unicode = str
@@ -116,15 +119,18 @@ try:
bytearray bytearray
except NameError: except NameError:
import array import array
class bytearray: class bytearray:
def __init__(self, val = ''): def __init__(self, val=""):
self.arr = array.array('B', val) self.arr = array.array("B", val)
self.append = self.arr.append self.append = self.arr.append
self.__getitem__ = self.arr.__getitem__ self.__getitem__ = self.arr.__getitem__
self.__len__ = self.arr.__len__ self.__len__ = self.arr.__len__
def decode(self, *args): def decode(self, *args):
return self.arr.tostring().decode(*args) return self.arr.tostring().decode(*args)
# Optimized .readinto() requires memoryview # Optimized .readinto() requires memoryview
try: try:
memoryview memoryview
@@ -136,21 +142,27 @@ except NameError:
try: try:
from struct import Struct from struct import Struct
except ImportError: except ImportError:
class Struct: class Struct:
def __init__(self, fmt): def __init__(self, fmt):
self.format = fmt self.format = fmt
self.size = struct.calcsize(fmt) self.size = struct.calcsize(fmt)
def unpack(self, buf): def unpack(self, buf):
return unpack(self.format, buf) return unpack(self.format, buf)
def unpack_from(self, buf, ofs=0): def unpack_from(self, buf, ofs=0):
return unpack(self.format, buf[ofs : ofs + self.size]) return unpack(self.format, buf[ofs : ofs + self.size])
def pack(self, *args): def pack(self, *args):
return pack(self.format, *args) return pack(self.format, *args)
# file object superclass # file object superclass
try: try:
from io import RawIOBase from io import RawIOBase
except ImportError: except ImportError:
class RawIOBase(object): class RawIOBase(object):
def close(self): def close(self):
pass pass
@@ -164,19 +176,19 @@ except ImportError:
DEFAULT_CHARSET = "windows-1252" DEFAULT_CHARSET = "windows-1252"
#: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed #: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed
TRY_ENCODINGS = ('utf8', 'utf-16le') TRY_ENCODINGS = ("utf8", "utf-16le")
#: 'unrar', 'rar' or full path to either one #: 'unrar', 'rar' or full path to either one
UNRAR_TOOL = "unrar" UNRAR_TOOL = "unrar"
#: Command line args to use for opening file for reading. #: Command line args to use for opening file for reading.
OPEN_ARGS = ('p', '-inul') OPEN_ARGS = ("p", "-inul")
#: Command line args to use for extracting file to disk. #: Command line args to use for extracting file to disk.
EXTRACT_ARGS = ('x', '-y', '-idq') EXTRACT_ARGS = ("x", "-y", "-idq")
#: args for testrar() #: args for testrar()
TEST_ARGS = ('t', '-idq') TEST_ARGS = ("t", "-idq")
# #
# Allow use of tool that is not compatible with unrar. # Allow use of tool that is not compatible with unrar.
@@ -189,11 +201,11 @@ TEST_ARGS = ('t', '-idq')
# - Does not support password-protected archives. # - Does not support password-protected archives.
# #
ALT_TOOL = 'bsdtar' ALT_TOOL = "bsdtar"
ALT_OPEN_ARGS = ('-x', '--to-stdout', '-f') ALT_OPEN_ARGS = ("-x", "--to-stdout", "-f")
ALT_EXTRACT_ARGS = ('-x', '-f') ALT_EXTRACT_ARGS = ("-x", "-f")
ALT_TEST_ARGS = ('-t', '-f') ALT_TEST_ARGS = ("-t", "-f")
ALT_CHECK_ARGS = ('--help',) ALT_CHECK_ARGS = ("--help",)
#: whether to speed up decompression by using tmp archive #: whether to speed up decompression by using tmp archive
USE_EXTRACT_HACK = 1 USE_EXTRACT_HACK = 1
@@ -212,7 +224,7 @@ USE_DATETIME = 0
#: Separator for path name components. RAR internally uses '\\'. #: Separator for path name components. RAR internally uses '\\'.
#: Use '/' to be similar with zipfile. #: Use '/' to be similar with zipfile.
PATH_SEP = '\\' PATH_SEP = "\\"
## ##
## rar constants ## rar constants
@@ -227,8 +239,8 @@ RAR_BLOCK_OLD_EXTRA = 0x76 # v
RAR_BLOCK_OLD_SUB = 0x77 # w RAR_BLOCK_OLD_SUB = 0x77 # w
RAR_BLOCK_OLD_RECOVERY = 0x78 # x RAR_BLOCK_OLD_RECOVERY = 0x78 # x
RAR_BLOCK_OLD_AUTH = 0x79 # y RAR_BLOCK_OLD_AUTH = 0x79 # y
RAR_BLOCK_SUB = 0x7a # z RAR_BLOCK_SUB = 0x7A # z
RAR_BLOCK_ENDARC = 0x7b # { RAR_BLOCK_ENDARC = 0x7B # {
# flags for RAR_BLOCK_MAIN # flags for RAR_BLOCK_MAIN
RAR_MAIN_VOLUME = 0x0001 RAR_MAIN_VOLUME = 0x0001
@@ -248,15 +260,15 @@ RAR_FILE_SPLIT_AFTER = 0x0002
RAR_FILE_PASSWORD = 0x0004 RAR_FILE_PASSWORD = 0x0004
RAR_FILE_COMMENT = 0x0008 RAR_FILE_COMMENT = 0x0008
RAR_FILE_SOLID = 0x0010 RAR_FILE_SOLID = 0x0010
RAR_FILE_DICTMASK = 0x00e0 RAR_FILE_DICTMASK = 0x00E0
RAR_FILE_DICT64 = 0x0000 RAR_FILE_DICT64 = 0x0000
RAR_FILE_DICT128 = 0x0020 RAR_FILE_DICT128 = 0x0020
RAR_FILE_DICT256 = 0x0040 RAR_FILE_DICT256 = 0x0040
RAR_FILE_DICT512 = 0x0060 RAR_FILE_DICT512 = 0x0060
RAR_FILE_DICT1024 = 0x0080 RAR_FILE_DICT1024 = 0x0080
RAR_FILE_DICT2048 = 0x00a0 RAR_FILE_DICT2048 = 0x00A0
RAR_FILE_DICT4096 = 0x00c0 RAR_FILE_DICT4096 = 0x00C0
RAR_FILE_DIRECTORY = 0x00e0 RAR_FILE_DIRECTORY = 0x00E0
RAR_FILE_LARGE = 0x0100 RAR_FILE_LARGE = 0x0100
RAR_FILE_UNICODE = 0x0200 RAR_FILE_UNICODE = 0x0200
RAR_FILE_SALT = 0x0400 RAR_FILE_SALT = 0x0400
@@ -294,71 +306,116 @@ RAR_M5 = 0x35
## internal constants ## internal constants
## ##
RAR_ID = bytes("Rar!\x1a\x07\x00", 'ascii') RAR_ID = bytes("Rar!\x1a\x07\x00", "ascii")
ZERO = bytes("\0", 'ascii') ZERO = bytes("\0", "ascii")
EMPTY = bytes("", 'ascii') EMPTY = bytes("", "ascii")
S_BLK_HDR = Struct('<HBHH') S_BLK_HDR = Struct("<HBHH")
S_FILE_HDR = Struct('<LLBLLBBHL') S_FILE_HDR = Struct("<LLBLLBBHL")
S_LONG = Struct('<L') S_LONG = Struct("<L")
S_SHORT = Struct('<H') S_SHORT = Struct("<H")
S_BYTE = Struct('<B') S_BYTE = Struct("<B")
S_COMMENT_HDR = Struct('<HBBH') S_COMMENT_HDR = Struct("<HBBH")
## ##
## Public interface ## Public interface
## ##
class Error(Exception): class Error(Exception):
"""Base class for rarfile errors.""" """Base class for rarfile errors."""
class BadRarFile(Error): class BadRarFile(Error):
"""Incorrect data in archive.""" """Incorrect data in archive."""
class NotRarFile(Error): class NotRarFile(Error):
"""The file is not RAR archive.""" """The file is not RAR archive."""
class BadRarName(Error): class BadRarName(Error):
"""Cannot guess multipart name components.""" """Cannot guess multipart name components."""
class NoRarEntry(Error): class NoRarEntry(Error):
"""File not found in RAR""" """File not found in RAR"""
class PasswordRequired(Error): class PasswordRequired(Error):
"""File requires password""" """File requires password"""
class NeedFirstVolume(Error): class NeedFirstVolume(Error):
"""Need to start from first volume.""" """Need to start from first volume."""
class NoCrypto(Error): class NoCrypto(Error):
"""Cannot parse encrypted headers - no crypto available.""" """Cannot parse encrypted headers - no crypto available."""
class RarExecError(Error): class RarExecError(Error):
"""Problem reported by unrar/rar.""" """Problem reported by unrar/rar."""
class RarWarning(RarExecError): class RarWarning(RarExecError):
"""Non-fatal error""" """Non-fatal error"""
class RarFatalError(RarExecError): class RarFatalError(RarExecError):
"""Fatal error""" """Fatal error"""
class RarCRCError(RarExecError): class RarCRCError(RarExecError):
"""CRC error during unpacking""" """CRC error during unpacking"""
class RarLockedArchiveError(RarExecError): class RarLockedArchiveError(RarExecError):
"""Must not modify locked archive""" """Must not modify locked archive"""
class RarWriteError(RarExecError): class RarWriteError(RarExecError):
"""Write error""" """Write error"""
class RarOpenError(RarExecError): class RarOpenError(RarExecError):
"""Open error""" """Open error"""
class RarUserError(RarExecError): class RarUserError(RarExecError):
"""User error""" """User error"""
class RarMemoryError(RarExecError): class RarMemoryError(RarExecError):
"""Memory error""" """Memory error"""
class RarCreateError(RarExecError): class RarCreateError(RarExecError):
"""Create error""" """Create error"""
class RarNoFilesError(RarExecError): class RarNoFilesError(RarExecError):
"""No files that match pattern were found""" """No files that match pattern were found"""
class RarUserBreak(RarExecError): class RarUserBreak(RarExecError):
"""User stop""" """User stop"""
class RarUnknownError(RarExecError): class RarUnknownError(RarExecError):
"""Unknown exit code""" """Unknown exit code"""
class RarSignalExit(RarExecError): class RarSignalExit(RarExecError):
"""Unrar exited with signal""" """Unrar exited with signal"""
class RarCannotExec(RarExecError): class RarCannotExec(RarExecError):
"""Executable not found.""" """Executable not found."""
def is_rarfile(xfile): def is_rarfile(xfile):
'''Check quickly whether file is rar archive.''' """Check quickly whether file is rar archive."""
fd = XFile(xfile) fd = XFile(xfile)
buf = fd.read(len(RAR_ID)) buf = fd.read(len(RAR_ID))
fd.close() fd.close()
@@ -366,7 +423,7 @@ def is_rarfile(xfile):
class RarInfo(object): class RarInfo(object):
r'''An entry in rar archive. r"""An entry in rar archive.
:mod:`zipfile`-compatible fields: :mod:`zipfile`-compatible fields:
@@ -417,49 +474,46 @@ class RarInfo(object):
One of RAR_BLOCK_* types. Only entries with type==RAR_BLOCK_FILE are shown in .infolist(). One of RAR_BLOCK_* types. Only entries with type==RAR_BLOCK_FILE are shown in .infolist().
flags flags
For files, RAR_FILE_* bits. For files, RAR_FILE_* bits.
''' """
__slots__ = ( __slots__ = (
# zipfile-compatible fields # zipfile-compatible fields
'filename', "filename",
'file_size', "file_size",
'compress_size', "compress_size",
'date_time', "date_time",
'comment', "comment",
'CRC', "CRC",
'volume', "volume",
'orig_filename', # bytes in unknown encoding "orig_filename", # bytes in unknown encoding
# rar-specific fields # rar-specific fields
'extract_version', "extract_version",
'compress_type', "compress_type",
'host_os', "host_os",
'mode', "mode",
'type', "type",
'flags', "flags",
# optional extended time fields # optional extended time fields
# tuple where the sec is float, or datetime(). # tuple where the sec is float, or datetime().
'mtime', # same as .date_time "mtime", # same as .date_time
'ctime', "ctime",
'atime', "atime",
'arctime', "arctime",
# RAR internals # RAR internals
'name_size', "name_size",
'header_size', "header_size",
'header_crc', "header_crc",
'file_offset', "file_offset",
'add_size', "add_size",
'header_data', "header_data",
'header_base', "header_base",
'header_offset', "header_offset",
'salt', "salt",
'volume_file', "volume_file",
) )
def isdir(self): def isdir(self):
'''Returns True if the entry is a directory.''' """Returns True if the entry is a directory."""
if self.type == RAR_BLOCK_FILE: if self.type == RAR_BLOCK_FILE:
return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY return (self.flags & RAR_FILE_DIRECTORY) == RAR_FILE_DIRECTORY
return False return False
@@ -469,15 +523,14 @@ class RarInfo(object):
class RarFile(object): class RarFile(object):
'''Parse RAR structure, provide access to files in archive. """Parse RAR structure, provide access to files in archive.
''' """
#: Archive comment. Byte string or None. Use :data:`UNICODE_COMMENTS` #: Archive comment. Byte string or None. Use :data:`UNICODE_COMMENTS`
#: to get automatic decoding to unicode. #: to get automatic decoding to unicode.
comment = None comment = None
def __init__(self, rarfile, mode="r", charset=None, info_callback=None, def __init__(self, rarfile, mode="r", charset=None, info_callback=None, crc_check=True, errors="stop"):
crc_check = True, errors = "stop"):
"""Open and parse a RAR archive. """Open and parse a RAR archive.
Parameters: Parameters:
@@ -529,39 +582,39 @@ class RarFile(object):
self.close() self.close()
def setpassword(self, password): def setpassword(self, password):
'''Sets the password to use when extracting.''' """Sets the password to use when extracting."""
self._password = password self._password = password
if not self._main: if not self._main:
self._parse() self._parse()
def needs_password(self): def needs_password(self):
'''Returns True if any archive entries require password for extraction.''' """Returns True if any archive entries require password for extraction."""
return self._needs_password return self._needs_password
def namelist(self): def namelist(self):
'''Return list of filenames in archive.''' """Return list of filenames in archive."""
return [f.filename for f in self._info_list] return [f.filename for f in self._info_list]
def infolist(self): def infolist(self):
'''Return RarInfo objects for all files/directories in archive.''' """Return RarInfo objects for all files/directories in archive."""
return self._info_list return self._info_list
def volumelist(self): def volumelist(self):
'''Returns filenames of archive volumes. """Returns filenames of archive volumes.
In case of single-volume archive, the list contains In case of single-volume archive, the list contains
just the name of main archive file. just the name of main archive file.
''' """
return self._vol_list return self._vol_list
def getinfo(self, fname): def getinfo(self, fname):
'''Return RarInfo for file.''' """Return RarInfo for file."""
if isinstance(fname, RarInfo): if isinstance(fname, RarInfo):
return fname return fname
# accept both ways here # accept both ways here
if PATH_SEP == '/': if PATH_SEP == "/":
fname2 = fname.replace("\\", "/") fname2 = fname.replace("\\", "/")
else: else:
fname2 = fname.replace("/", "\\") fname2 = fname.replace("/", "\\")
@@ -574,8 +627,8 @@ class RarFile(object):
except KeyError: except KeyError:
raise NoRarEntry("No such file: " + fname) raise NoRarEntry("No such file: " + fname)
def open(self, fname, mode = 'r', psw = None): def open(self, fname, mode="r", psw=None):
'''Returns file-like object (:class:`RarExtFile`), """Returns file-like object (:class:`RarExtFile`),
from where the data can be read. from where the data can be read.
The object implements :class:`io.RawIOBase` interface, so it can The object implements :class:`io.RawIOBase` interface, so it can
@@ -597,9 +650,9 @@ class RarFile(object):
must be 'r' must be 'r'
psw psw
password to use for extracting. password to use for extracting.
''' """
if mode != 'r': if mode != "r":
raise NotImplementedError("RarFile.open() supports only mode=r") raise NotImplementedError("RarFile.open() supports only mode=r")
# entry lookup # entry lookup
@@ -654,7 +707,7 @@ class RarFile(object):
password to use for extracting. password to use for extracting.
""" """
f = self.open(fname, 'r', psw) f = self.open(fname, "r", psw)
try: try:
return f.read() return f.read()
finally: finally:
@@ -752,7 +805,7 @@ class RarFile(object):
old.compress_size += item.compress_size old.compress_size += item.compress_size
# parse new-style comment # parse new-style comment
if item.type == RAR_BLOCK_SUB and item.filename == 'CMT': if item.type == RAR_BLOCK_SUB and item.filename == "CMT":
if not NEED_COMMENTS: if not NEED_COMMENTS:
pass pass
elif item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): elif item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER):
@@ -849,9 +902,10 @@ class RarFile(object):
# AES encrypted headers # AES encrypted headers
_last_aes_key = (None, None, None) # (salt, key, iv) _last_aes_key = (None, None, None) # (salt, key, iv)
def _decrypt_header(self, fd): def _decrypt_header(self, fd):
if not _have_crypto: if not _have_crypto:
raise NoCrypto('Cannot parse encrypted headers - no crypto') raise NoCrypto("Cannot parse encrypted headers - no crypto")
salt = fd.read(8) salt = fd.read(8)
if self._last_aes_key[0] == salt: if self._last_aes_key[0] == salt:
key, iv = self._last_aes_key[1:] key, iv = self._last_aes_key[1:]
@@ -872,7 +926,7 @@ class RarFile(object):
# now read actual header # now read actual header
return self._parse_block_header(fd) return self._parse_block_header(fd)
except struct.error: except struct.error:
self._set_error('Broken header in RAR file') self._set_error("Broken header in RAR file")
return None return None
# common header # common header
@@ -899,7 +953,7 @@ class RarFile(object):
# unexpected EOF? # unexpected EOF?
if len(h.header_data) != h.header_size: if len(h.header_data) != h.header_size:
self._set_error('Unexpected EOF when reading header') self._set_error("Unexpected EOF when reading header")
return None return None
# block has data assiciated with it? # block has data assiciated with it?
@@ -943,8 +997,9 @@ class RarFile(object):
return h return h
# header parsing failed. # header parsing failed.
self._set_error('Header CRC error (%02x): exp=%x got=%x (xlen = %d)', self._set_error(
h.type, h.header_crc, calc_crc, len(crcdat)) "Header CRC error (%02x): exp=%x got=%x (xlen = %d)", h.type, h.header_crc, calc_crc, len(crcdat)
)
# instead panicing, send eof # instead panicing, send eof
return None return None
@@ -987,8 +1042,8 @@ class RarFile(object):
h.filename = self._decode(name) h.filename = self._decode(name)
# change separator, if requested # change separator, if requested
if PATH_SEP != '\\': if PATH_SEP != "\\":
h.filename = h.filename.replace('\\', PATH_SEP) h.filename = h.filename.replace("\\", PATH_SEP)
if h.flags & RAR_FILE_SALT: if h.flags & RAR_FILE_SALT:
h.salt = h.header_data[pos : pos + 8] h.salt = h.header_data[pos : pos + 8]
@@ -1045,8 +1100,7 @@ class RarFile(object):
declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos) declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos)
pos += S_COMMENT_HDR.size pos += S_COMMENT_HDR.size
data = hdata[pos:pos_next] data = hdata[pos:pos_next]
cmt = rar_decompress(ver, meth, data, declen, sflags, cmt = rar_decompress(ver, meth, data, declen, sflags, crc, self._password)
crc, self._password)
if not self._crc_check: if not self._crc_check:
h.comment = self._decode_comment(cmt) h.comment = self._decode_comment(cmt)
elif crc32(cmt) & 0xFFFF == crc: elif crc32(cmt) & 0xFFFF == crc:
@@ -1100,7 +1154,7 @@ class RarFile(object):
def _next_newvol(self, volfile): def _next_newvol(self, volfile):
i = len(volfile) - 1 i = len(volfile) - 1
while i >= 0: while i >= 0:
if volfile[i] >= '0' and volfile[i] <= '9': if volfile[i] >= "0" and volfile[i] <= "9":
return self._inc_volname(volfile, i) return self._inc_volname(volfile, i)
i -= 1 i -= 1
raise BadRarName("Cannot construct volume name: " + volfile) raise BadRarName("Cannot construct volume name: " + volfile)
@@ -1108,20 +1162,20 @@ class RarFile(object):
# old-style next volume # old-style next volume
def _next_oldvol(self, volfile): def _next_oldvol(self, volfile):
# rar -> r00 # rar -> r00
if volfile[-4:].lower() == '.rar': if volfile[-4:].lower() == ".rar":
return volfile[:-2] + '00' return volfile[:-2] + "00"
return self._inc_volname(volfile, len(volfile) - 1) return self._inc_volname(volfile, len(volfile) - 1)
# increase digits with carry, otherwise just increment char # increase digits with carry, otherwise just increment char
def _inc_volname(self, volfile, i): def _inc_volname(self, volfile, i):
fn = list(volfile) fn = list(volfile)
while i >= 0: while i >= 0:
if fn[i] != '9': if fn[i] != "9":
fn[i] = chr(ord(fn[i]) + 1) fn[i] = chr(ord(fn[i]) + 1)
break break
fn[i] = '0' fn[i] = "0"
i -= 1 i -= 1
return ''.join(fn) return "".join(fn)
def _open_clear(self, inf): def _open_clear(self, inf):
return DirectReader(self, inf) return DirectReader(self, inf)
@@ -1135,7 +1189,7 @@ class RarFile(object):
rf = XFile(inf.volume_file, 0) rf = XFile(inf.volume_file, 0)
rf.seek(inf.header_offset) rf.seek(inf.header_offset)
tmpfd, tmpname = mkstemp(suffix='.rar') tmpfd, tmpname = mkstemp(suffix=".rar")
tmpf = os.fdopen(tmpfd, "wb") tmpf = os.fdopen(tmpfd, "wb")
try: try:
@@ -1148,7 +1202,7 @@ class RarFile(object):
else: else:
buf = rf.read(size) buf = rf.read(size)
if not buf: if not buf:
raise BadRarFile('read failed: ' + inf.filename) raise BadRarFile("read failed: " + inf.filename)
tmpf.write(buf) tmpf.write(buf)
size -= len(buf) size -= len(buf)
tmpf.close() tmpf.close()
@@ -1170,14 +1224,15 @@ class RarFile(object):
rf.close() rf.close()
# decompress # decompress
cmt = rar_decompress(inf.extract_version, inf.compress_type, data, cmt = rar_decompress(
inf.file_size, inf.flags, inf.CRC, psw, inf.salt) inf.extract_version, inf.compress_type, data, inf.file_size, inf.flags, inf.CRC, psw, inf.salt
)
# check crc # check crc
if self._crc_check: if self._crc_check:
crc = crc32(cmt) crc = crc32(cmt)
if crc < 0: if crc < 0:
crc += (long(1) << 32) crc += long(1) << 32
if crc != inf.CRC: if crc != inf.CRC:
return None return None
@@ -1208,7 +1263,7 @@ class RarFile(object):
return val.decode(c) return val.decode(c)
except UnicodeError: except UnicodeError:
pass pass
return val.decode(self._charset, 'replace') return val.decode(self._charset, "replace")
def _decode_comment(self, val): def _decode_comment(self, val):
if UNICODE_COMMENTS: if UNICODE_COMMENTS:
@@ -1241,10 +1296,12 @@ class RarFile(object):
output = p.communicate()[0] output = p.communicate()[0]
check_returncode(p, output) check_returncode(p, output)
## ##
## Utility classes ## Utility classes
## ##
class UnicodeFilename: class UnicodeFilename:
"""Handle unicode filename decompression""" """Handle unicode filename decompression"""
@@ -1269,7 +1326,7 @@ class UnicodeFilename:
return self.std_name[self.pos] return self.std_name[self.pos]
except IndexError: except IndexError:
self.failed = 1 self.failed = 1
return ord('?') return ord("?")
def put(self, lo, hi): def put(self, lo, hi):
self.buf.append(lo) self.buf.append(lo)
@@ -1295,7 +1352,7 @@ class UnicodeFilename:
n = self.enc_byte() n = self.enc_byte()
if n & 0x80: if n & 0x80:
c = self.enc_byte() c = self.enc_byte()
for i in range((n & 0x7f) + 2): for i in range((n & 0x7F) + 2):
lo = (self.std_byte() + c) & 0xFF lo = (self.std_byte() + c) & 0xFF
self.put(lo, hi) self.put(lo, hi)
else: else:
@@ -1326,7 +1383,7 @@ class RarExtFile(RawIOBase):
# standard io.* properties # standard io.* properties
self.name = inf.filename self.name = inf.filename
self.mode = 'rb' self.mode = "rb"
self.rf = rf self.rf = rf
self.inf = inf self.inf = inf
@@ -1375,12 +1432,12 @@ class RarExtFile(RawIOBase):
if not self.crc_check: if not self.crc_check:
return return
if self.returncode: if self.returncode:
check_returncode(self, '') check_returncode(self, "")
if self.remain != 0: if self.remain != 0:
raise BadRarFile("Failed the read enough data") raise BadRarFile("Failed the read enough data")
crc = self.CRC crc = self.CRC
if crc < 0: if crc < 0:
crc += (long(1) << 32) crc += long(1) << 32
if crc != self.inf.CRC: if crc != self.inf.CRC:
raise BadRarFile("Corrupt file - CRC check failed: " + self.inf.filename) raise BadRarFile("Corrupt file - CRC check failed: " + self.inf.filename)
@@ -1412,6 +1469,7 @@ class RarExtFile(RawIOBase):
buf[:n] = data buf[:n] = data
except TypeError: except TypeError:
import array import array
if not isinstance(buf, array.array): if not isinstance(buf, array.array):
raise raise
buf[:n] = array.array(buf.typecode, data) buf[:n] = array.array(buf.typecode, data)
@@ -1443,7 +1501,7 @@ class RarExtFile(RawIOBase):
elif whence == 2: # seek from end of file elif whence == 2: # seek from end of file
new_ofs = fsize + ofs new_ofs = fsize + ofs
else: else:
raise ValueError('Invalid value for whence') raise ValueError("Invalid value for whence")
# sanity check # sanity check
if new_ofs < 0: if new_ofs < 0:
@@ -1566,6 +1624,7 @@ class PipeReader(RarExtFile):
self.tempfile = None self.tempfile = None
if have_memoryview: if have_memoryview:
def readinto(self, buf): def readinto(self, buf):
"""Zero-copy read directly into buffer.""" """Zero-copy read directly into buffer."""
cnt = len(buf) cnt = len(buf)
@@ -1676,6 +1735,7 @@ class DirectReader(RarExtFile):
return True return True
if have_memoryview: if have_memoryview:
def readinto(self, buf): def readinto(self, buf):
"""Zero-copy read directly into buffer.""" """Zero-copy read directly into buffer."""
got = 0 got = 0
@@ -1705,6 +1765,7 @@ class DirectReader(RarExtFile):
class HeaderDecrypt: class HeaderDecrypt:
"""File-like object that decrypts from another file""" """File-like object that decrypts from another file"""
def __init__(self, f, key, iv): def __init__(self, f, key, iv):
self.f = f self.f = f
self.ciph = AES.new(key, AES.MODE_CBC, iv) self.ciph = AES.new(key, AES.MODE_CBC, iv)
@@ -1715,7 +1776,7 @@ class HeaderDecrypt:
def read(self, cnt=None): def read(self, cnt=None):
if cnt > 8 * 1024: if cnt > 8 * 1024:
raise BadRarFile('Bad count to header decrypt - wrong password?') raise BadRarFile("Bad count to header decrypt - wrong password?")
# consume old data # consume old data
if cnt <= len(self.buf): if cnt <= len(self.buf):
@@ -1743,9 +1804,11 @@ class HeaderDecrypt:
return res return res
# handle (filename|filelike) object # handle (filename|filelike) object
class XFile(object): class XFile(object):
__slots__ = ('_fd', '_need_close') __slots__ = ("_fd", "_need_close")
def __init__(self, xfile, bufsize=1024): def __init__(self, xfile, bufsize=1024):
if is_filelike(xfile): if is_filelike(xfile):
self._need_close = False self._need_close = False
@@ -1753,41 +1816,51 @@ class XFile(object):
self._fd.seek(0) self._fd.seek(0)
else: else:
self._need_close = True self._need_close = True
self._fd = open(xfile, 'rb', bufsize) self._fd = open(xfile, "rb", bufsize)
def read(self, n=None): def read(self, n=None):
return self._fd.read(n) return self._fd.read(n)
def tell(self): def tell(self):
return self._fd.tell() return self._fd.tell()
def seek(self, ofs, whence=0): def seek(self, ofs, whence=0):
return self._fd.seek(ofs, whence) return self._fd.seek(ofs, whence)
def readinto(self, dst): def readinto(self, dst):
return self._fd.readinto(dst) return self._fd.readinto(dst)
def close(self): def close(self):
if self._need_close: if self._need_close:
self._fd.close() self._fd.close()
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, typ, val, tb): def __exit__(self, typ, val, tb):
self.close() self.close()
## ##
## Utility functions ## Utility functions
## ##
def is_filelike(obj): def is_filelike(obj):
if isinstance(obj, str) or isinstance(obj, unicode): if isinstance(obj, str) or isinstance(obj, unicode):
return False return False
res = True res = True
for a in ('read', 'tell', 'seek'): for a in ("read", "tell", "seek"):
res = res and hasattr(obj, a) res = res and hasattr(obj, a)
if not res: if not res:
raise ValueError("Invalid object passed as file") raise ValueError("Invalid object passed as file")
return True return True
def rar3_s2k(psw, salt): def rar3_s2k(psw, salt):
"""String-to-key hash for RAR3.""" """String-to-key hash for RAR3."""
seed = psw.encode('utf-16le') + salt seed = psw.encode("utf-16le") + salt
iv = EMPTY iv = EMPTY
h = sha1() h = sha1()
for i in range(16): for i in range(16):
@@ -1800,6 +1873,7 @@ def rar3_s2k(psw, salt):
key_le = pack("<LLLL", *unpack(">LLLL", key_be)) key_le = pack("<LLLL", *unpack(">LLLL", key_be))
return key_le, iv return key_le, iv
def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None): def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None):
"""Decompress blob of compressed data. """Decompress blob of compressed data.
@@ -1815,11 +1889,10 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No
flags |= RAR_LONG_BLOCK flags |= RAR_LONG_BLOCK
# file header # file header
fname = bytes('data', 'ascii') fname = bytes("data", "ascii")
date = 0 date = 0
mode = 0x20 mode = 0x20
fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc, fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc, date, vers, meth, len(fname), mode)
date, vers, meth, len(fname), mode)
fhdr += fname fhdr += fname
if flags & RAR_FILE_SALT: if flags & RAR_FILE_SALT:
if not salt: if not salt:
@@ -1836,7 +1909,7 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No
mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4) mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4)
# decompress via temp rar # decompress via temp rar
tmpfd, tmpname = mkstemp(suffix='.rar') tmpfd, tmpname = mkstemp(suffix=".rar")
tmpf = os.fdopen(tmpfd, "wb") tmpf = os.fdopen(tmpfd, "wb")
try: try:
tmpf.write(RAR_ID + mh + hdr + data) tmpf.write(RAR_ID + mh + hdr + data)
@@ -1852,6 +1925,7 @@ def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=No
tmpf.close() tmpf.close()
os.unlink(tmpname) os.unlink(tmpname)
def to_datetime(t): def to_datetime(t):
"""Convert 6-part time tuple into datetime object.""" """Convert 6-part time tuple into datetime object."""
@@ -1871,13 +1945,20 @@ def to_datetime(t):
# sanitize invalid values # sanitize invalid values
MDAY = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) MDAY = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
if mon < 1: mon = 1 if mon < 1:
if mon > 12: mon = 12 mon = 1
if day < 1: day = 1 if mon > 12:
if day > MDAY[mon]: day = MDAY[mon] mon = 12
if h > 23: h = 23 if day < 1:
if m > 59: m = 59 day = 1
if s > 59: s = 59 if day > MDAY[mon]:
day = MDAY[mon]
if h > 23:
h = 23
if m > 59:
m = 59
if s > 59:
s = 59
if mon == 2 and day == 29: if mon == 2 and day == 29:
try: try:
return datetime(year, mon, day, h, m, s, us) return datetime(year, mon, day, h, m, s, us)
@@ -1885,30 +1966,35 @@ def to_datetime(t):
day = 28 day = 28
return datetime(year, mon, day, h, m, s, us) return datetime(year, mon, day, h, m, s, us)
def parse_dos_time(stamp): def parse_dos_time(stamp):
"""Parse standard 32-bit DOS timestamp.""" """Parse standard 32-bit DOS timestamp."""
sec = stamp & 0x1F; stamp = stamp >> 5 sec = stamp & 0x1F
min = stamp & 0x3F; stamp = stamp >> 6 stamp = stamp >> 5
hr = stamp & 0x1F; stamp = stamp >> 5 min = stamp & 0x3F
day = stamp & 0x1F; stamp = stamp >> 5 stamp = stamp >> 6
mon = stamp & 0x0F; stamp = stamp >> 4 hr = stamp & 0x1F
stamp = stamp >> 5
day = stamp & 0x1F
stamp = stamp >> 5
mon = stamp & 0x0F
stamp = stamp >> 4
yr = (stamp & 0x7F) + 1980 yr = (stamp & 0x7F) + 1980
return (yr, mon, day, hr, min, sec * 2) return (yr, mon, day, hr, min, sec * 2)
def custom_popen(cmd): def custom_popen(cmd):
"""Disconnect cmd from parent fds, read only from stdout.""" """Disconnect cmd from parent fds, read only from stdout."""
# needed for py2exe # needed for py2exe
creationflags = 0 creationflags = 0
if sys.platform == 'win32': if sys.platform == "win32":
creationflags = 0x08000000 # CREATE_NO_WINDOW creationflags = 0x08000000 # CREATE_NO_WINDOW
# run command # run command
try: try:
p = Popen(cmd, bufsize = 0, p = Popen(cmd, bufsize=0, stdout=PIPE, stdin=PIPE, stderr=STDOUT, creationflags=creationflags)
stdout = PIPE, stdin = PIPE, stderr = STDOUT,
creationflags = creationflags)
except OSError: except OSError:
ex = sys.exc_info()[1] ex = sys.exc_info()[1]
if ex.errno == errno.ENOENT: if ex.errno == errno.ENOENT:
@@ -1916,6 +2002,7 @@ def custom_popen(cmd):
raise raise
return p return p
def custom_check(cmd, ignore_retcode=False): def custom_check(cmd, ignore_retcode=False):
"""Run command, collect output, raise error if needed.""" """Run command, collect output, raise error if needed."""
p = custom_popen(cmd) p = custom_popen(cmd)
@@ -1924,14 +2011,16 @@ def custom_check(cmd, ignore_retcode=False):
raise RarExecError("Check-run failed") raise RarExecError("Check-run failed")
return out return out
def add_password_arg(cmd, psw, required=False): def add_password_arg(cmd, psw, required=False):
"""Append password switch to commandline.""" """Append password switch to commandline."""
if UNRAR_TOOL == ALT_TOOL: if UNRAR_TOOL == ALT_TOOL:
return return
if psw is not None: if psw is not None:
cmd.append('-p' + psw) cmd.append("-p" + psw)
else: else:
cmd.append('-p-') cmd.append("-p-")
def check_returncode(p, out): def check_returncode(p, out):
"""Raise exception according to unrar exit code""" """Raise exception according to unrar exit code"""
@@ -1941,10 +2030,19 @@ def check_returncode(p, out):
return return
# map return code to exception class # map return code to exception class
errmap = [None, errmap = [
RarWarning, RarFatalError, RarCRCError, RarLockedArchiveError, None,
RarWriteError, RarOpenError, RarUserError, RarMemoryError, RarWarning,
RarCreateError, RarNoFilesError] # codes from rar.txt RarFatalError,
RarCRCError,
RarLockedArchiveError,
RarWriteError,
RarOpenError,
RarUserError,
RarMemoryError,
RarCreateError,
RarNoFilesError,
] # codes from rar.txt
if UNRAR_TOOL == ALT_TOOL: if UNRAR_TOOL == ALT_TOOL:
errmap = [None] errmap = [None]
if code > 0 and code < len(errmap): if code > 0 and code < len(errmap):
@@ -1964,6 +2062,7 @@ def check_returncode(p, out):
raise exc(msg) raise exc(msg)
# #
# Check if unrar works # Check if unrar works
# #
@@ -1983,4 +2082,3 @@ except RarCannotExec:
except RarCannotExec: except RarCannotExec:
# no usable tool, only uncompressed archives work # no usable tool, only uncompressed archives work
pass pass

View File

@@ -12,9 +12,7 @@ from comic.util import generate_directory
class ComicBookTests(TestCase): class ComicBookTests(TestCase):
def setUp(self): def setUp(self):
Setting.objects.create( Setting.objects.create(name="BASE_DIR", value=path.join(os.getcwd(), "comic", "test"))
name="BASE_DIR", value=path.join(os.getcwd(), "comic", "test")
)
User.objects.create_user("test", "test@test.com", "test") User.objects.create_user("test", "test@test.com", "test")
user = User.objects.first() user = User.objects.first()
ComicBook.process_comic_book("test1.rar") ComicBook.process_comic_book("test1.rar")
@@ -133,51 +131,34 @@ class ComicBookTests(TestCase):
d = Directory.objects.get(name="test_folder", parent__isnull=True) d = Directory.objects.get(name="test_folder", parent__isnull=True)
location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes)) location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes))
self.assertEqual(dir1.location, location) self.assertEqual(dir1.location, location)
self.assertEqual( self.assertEqual(dir1.label, '<center><span class="label label-default">Empty</span></center>')
dir1.label,
'<center><span class="label label-default">Empty</span></center>',
)
dir2 = folders[1] dir2 = folders[1]
self.assertEqual(dir2.name, "test1.rar") self.assertEqual(dir2.name, "test1.rar")
self.assertEqual(dir2.type, "book") self.assertEqual(dir2.type, "book")
self.assertEqual(dir2.icon, "glyphicon-book") self.assertEqual(dir2.icon, "glyphicon-book")
c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True) c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True)
location = "/comic/read/{0}/{1}/".format( location = "/comic/read/{0}/{1}/".format(urlsafe_base64_encode(c.selector.bytes), "0")
urlsafe_base64_encode(c.selector.bytes), "0"
)
self.assertEqual(dir2.location, location) self.assertEqual(dir2.location, location)
self.assertEqual( self.assertEqual(dir2.label, '<center><span class="label label-default">Unread</span></center>')
dir2.label,
'<center><span class="label label-default">Unread</span></center>',
)
dir3 = folders[2] dir3 = folders[2]
self.assertEqual(dir3.name, "test2.rar") self.assertEqual(dir3.name, "test2.rar")
self.assertEqual(dir3.type, "book") self.assertEqual(dir3.type, "book")
self.assertEqual(dir3.icon, "glyphicon-book") self.assertEqual(dir3.icon, "glyphicon-book")
c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True) c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True)
location = "/comic/read/{0}/{1}/".format( location = "/comic/read/{0}/{1}/".format(urlsafe_base64_encode(c.selector.bytes), "2")
urlsafe_base64_encode(c.selector.bytes), "2"
)
self.assertEqual(dir3.location, location) self.assertEqual(dir3.location, location)
self.assertEqual( self.assertEqual(dir3.label, '<center><span class="label label-primary">3/4</span></center>')
dir3.label, '<center><span class="label label-primary">3/4</span></center>'
)
dir4 = folders[3] dir4 = folders[3]
self.assertEqual(dir4.name, "test3.rar") self.assertEqual(dir4.name, "test3.rar")
self.assertEqual(dir4.type, "book") self.assertEqual(dir4.type, "book")
self.assertEqual(dir3.icon, "glyphicon-book") self.assertEqual(dir3.icon, "glyphicon-book")
c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True) c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True)
location = "/comic/read/{0}/{1}/".format( location = "/comic/read/{0}/{1}/".format(urlsafe_base64_encode(c.selector.bytes), "0")
urlsafe_base64_encode(c.selector.bytes), "0"
)
self.assertEqual(dir4.location, location) self.assertEqual(dir4.location, location)
self.assertEqual( self.assertEqual(dir4.label, '<center><span class="label label-default">Unread</span></center>')
dir4.label,
'<center><span class="label label-default">Unread</span></center>',
)
def test_pages(self): def test_pages(self):
book = ComicBook.objects.get(file_name="test1.rar") book = ComicBook.objects.get(file_name="test1.rar")
@@ -217,9 +198,7 @@ class ComicBookTests(TestCase):
response = c.post("/comic/list_json/") response = c.post("/comic/list_json/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
directory = Directory.objects.first() directory = Directory.objects.first()
response = c.post( response = c.post(f"/comic/list_json/{urlsafe_base64_encode(directory.selector.bytes)}/")
f"/comic/list_json/{urlsafe_base64_encode(directory.selector.bytes)}/"
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_recent_comics(self): def test_recent_comics(self):
@@ -241,12 +220,7 @@ class ComicBookTests(TestCase):
generate_directory(User.objects.first()) generate_directory(User.objects.first())
ComicStatus.objects.all().delete() ComicStatus.objects.all().delete()
req_data = { req_data = {"start": "0", "length": "10", "search[value]": "", "order[0][dir]": "desc"}
"start": "0",
"length": "10",
"search[value]": "",
"order[0][dir]": "desc",
}
response = c.post("/comic/recent/json/", req_data) response = c.post("/comic/recent/json/", req_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
req_data["search[value]"] = "test1.rar" req_data["search[value]"] = "test1.rar"
@@ -261,13 +235,11 @@ class ComicBookTests(TestCase):
{ {
"date": book.date_added.strftime("%d/%m/%y-%H:%M"), "date": book.date_added.strftime("%d/%m/%y-%H:%M"),
"icon": '<span class="glyphicon glyphicon-book"></span>', "icon": '<span class="glyphicon glyphicon-book"></span>',
"label": '<center><span class="label ' "label": '<center><span class="label ' 'label-default">Unread</span></center>',
'label-default">Unread</span></center>',
"name": "test1.rar", "name": "test1.rar",
"selector": urlsafe_base64_encode(book.selector.bytes), "selector": urlsafe_base64_encode(book.selector.bytes),
"type": "book", "type": "book",
"url": f"/comic/read/" "url": f"/comic/read/" f"{urlsafe_base64_encode(book.selector.bytes)}/0/",
f"{urlsafe_base64_encode(book.selector.bytes)}/0/",
} }
], ],
"recordsFiltered": 1, "recordsFiltered": 1,
@@ -302,11 +274,7 @@ class ComicBookTests(TestCase):
response = c.get("/comic/edit/") response = c.get("/comic/edit/")
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
req_data = { req_data = {"comic_list_length": 10, "func": "unread", "selected": book.selector_string}
"comic_list_length": 10,
"func": "unread",
"selected": book.selector_string,
}
response = c.post("/comic/edit/", req_data) response = c.post("/comic/edit/", req_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View File

@@ -2,20 +2,21 @@ from django.conf.urls import url
from . import feeds from . import feeds
from . import views from . import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.comic_list, name='index'), url(r"^$", views.comic_list, name="index"),
url(r'^settings/$', views.settings_page, name='settings'), url(r"^settings/$", views.settings_page, name="settings"),
url(r'^settings/users/$', views.users_page, name='users'), url(r"^settings/users/$", views.users_page, name="users"),
url(r'^settings/users/(?P<user_id>[0-9]+)/$', views.user_config_page, name='users'), url(r"^settings/users/(?P<user_id>[0-9]+)/$", views.user_config_page, name="users"),
url(r'^settings/users/add/$', views.user_add_page, name='users'), url(r"^settings/users/add/$", views.user_add_page, name="users"),
url(r'^account/$', views.account_page, name='account'), url(r"^account/$", views.account_page, name="account"),
url(r'^read/(?P<comic_selector>[\w-]+)/(?P<page>[0-9]+)/$', views.read_comic, name='read_comic'), url(r"^read/(?P<comic_selector>[\w-]+)/(?P<page>[0-9]+)/$", views.read_comic, name="read_comic"),
url(r'^read/(?P<comic_selector>[\w-]+)/(?P<page>[0-9]+)/img$', views.get_image, name='get_image'), url(r"^read/(?P<comic_selector>[\w-]+)/(?P<page>[0-9]+)/img$", views.get_image, name="get_image"),
url(r'^list_json/$', views.comic_list_json, name='comic_list_json1'), url(r"^list_json/$", views.comic_list_json, name="comic_list_json1"),
url(r'^list_json/(?P<directory_selector>[\w-]+)/$', views.comic_list_json, name='comic_list_json2'), url(r"^list_json/(?P<directory_selector>[\w-]+)/$", views.comic_list_json, name="comic_list_json2"),
url(r'^recent/$', views.recent_comics, name='recent_comics'), url(r"^recent/$", views.recent_comics, name="recent_comics"),
url(r'^recent/json/$', views.recent_comics_json, name='recent_comics_json'), url(r"^recent/json/$", views.recent_comics_json, name="recent_comics_json"),
url(r'^edit/$', views.comic_edit, name='comic_edit'), url(r"^edit/$", views.comic_edit, name="comic_edit"),
url(r'^feed/(?P<user_selector>[\w-]+)/$', feeds.RecentComics()), url(r"^feed/(?P<user_selector>[\w-]+)/$", feeds.RecentComics()),
url(r'^(?P<directory_selector>[\w-]+)/$', views.comic_list, name='comic_list'), url(r"^(?P<directory_selector>[\w-]+)/$", views.comic_list, name="comic_list"),
] ]

View File

@@ -8,32 +8,32 @@ from .models import ComicBook, Setting, ComicStatus, Directory
def generate_title_from_path(file_path): def generate_title_from_path(file_path):
if file_path == '': if file_path == "":
return 'CBWebReader' return "CBWebReader"
return 'CBWebReader - ' + ' - '.join(file_path.split(path.sep)) return "CBWebReader - " + " - ".join(file_path.split(path.sep))
class Menu: class Menu:
def __init__(self, user, page=''): def __init__(self, user, page=""):
""" """
:type page: str :type page: str
""" """
self.menu_items = OrderedDict() self.menu_items = OrderedDict()
self.menu_items['Browse'] = '/comic/' self.menu_items["Browse"] = "/comic/"
self.menu_items['Recent'] = '/comic/recent/' self.menu_items["Recent"] = "/comic/recent/"
self.menu_items['Account'] = '/comic/account/' self.menu_items["Account"] = "/comic/account/"
if user.is_superuser: if user.is_superuser:
self.menu_items['Settings'] = '/comic/settings/' self.menu_items["Settings"] = "/comic/settings/"
self.menu_items['Users'] = '/comic/settings/users/' self.menu_items["Users"] = "/comic/settings/users/"
self.menu_items['Logout'] = '/logout/' self.menu_items["Logout"] = "/logout/"
self.current_page = page self.current_page = page
class Breadcrumb: class Breadcrumb:
def __init__(self): def __init__(self):
self.name = 'Home' self.name = "Home"
self.url = '/comic/' self.url = "/comic/"
def __str__(self): def __str__(self):
return self.name return self.name
@@ -56,12 +56,12 @@ def generate_breadcrumbs_from_path(directory=False, book=False):
for item in folders[::-1]: for item in folders[::-1]:
bc = Breadcrumb() bc = Breadcrumb()
bc.name = item.name bc.name = item.name
bc.url = '/comic/' + urlsafe_base64_encode(item.selector.bytes) bc.url = "/comic/" + urlsafe_base64_encode(item.selector.bytes)
output.append(bc) output.append(bc)
if book: if book:
bc = Breadcrumb() bc = Breadcrumb()
bc.name = book.file_name bc.name = book.file_name
bc.url = '/read/' + urlsafe_base64_encode(book.selector.bytes) bc.url = "/read/" + urlsafe_base64_encode(book.selector.bytes)
output.append(bc) output.append(bc)
return output return output
@@ -79,46 +79,45 @@ def generate_breadcrumbs_from_menu(paths):
class DirFile: class DirFile:
def __init__(self): def __init__(self):
self.name = '' self.name = ""
self.icon = '' self.icon = ""
self.location = '' self.location = ""
self.label = '' self.label = ""
self.type = '' self.type = ""
self.selector = '' self.selector = ""
def __str__(self): def __str__(self):
return self.name return self.name
def populate_directory(self, directory, user): def populate_directory(self, directory, user):
self.name = directory.name self.name = directory.name
self.icon = 'glyphicon-folder-open' self.icon = "glyphicon-folder-open"
self.selector = urlsafe_base64_encode(directory.selector.bytes) self.selector = urlsafe_base64_encode(directory.selector.bytes)
self.location = '/comic/{0}/'.format(self.selector) self.location = "/comic/{0}/".format(self.selector)
self.label = generate_dir_status(user, directory) self.label = generate_dir_status(user, directory)
self.type = 'directory' self.type = "directory"
def populate_comic(self, comic, user): def populate_comic(self, comic, user):
if type(comic) == str: if type(comic) == str:
self.icon = 'glyphicon-remove' self.icon = "glyphicon-remove"
self.name = comic self.name = comic
self.selector = '0' self.selector = "0"
self.location = '/' self.location = "/"
self.label = '<center><span class="label label-danger">Error</span></center>' self.label = '<center><span class="label label-danger">Error</span></center>'
self.type = 'book' self.type = "book"
else: else:
self.icon = 'glyphicon-book' self.icon = "glyphicon-book"
self.name = comic.file_name self.name = comic.file_name
status, created = ComicStatus.objects.get_or_create(comic=comic, user=user) status, created = ComicStatus.objects.get_or_create(comic=comic, user=user)
if created: if created:
status.save() status.save()
self.selector = urlsafe_base64_encode(comic.selector.bytes) self.selector = urlsafe_base64_encode(comic.selector.bytes)
self.location = '/comic/read/{0}/{1}/'.format(self.selector, self.location = "/comic/read/{0}/{1}/".format(self.selector, status.last_read_page)
status.last_read_page)
self.label = generate_label(comic, status) self.label = generate_label(comic, status)
self.type = 'book' self.type = "book"
def __repr__(self): def __repr__(self):
return f'<DirFile: {self.name}: {self.type}>' return f"<DirFile: {self.name}: {self.type}>"
def generate_directory(user, directory=False): def generate_directory(user, directory=False):
@@ -126,7 +125,7 @@ def generate_directory(user, directory=False):
:type user: User :type user: User
:type directory: Directory :type directory: Directory
""" """
base_dir = Setting.objects.get(name='BASE_DIR').value base_dir = Setting.objects.get(name="BASE_DIR").value
files = [] files = []
if directory: if directory:
ordered_dir_list = listdir(path.join(base_dir, directory.path)) ordered_dir_list = listdir(path.join(base_dir, directory.path))
@@ -136,15 +135,11 @@ def generate_directory(user, directory=False):
dir_list = [x for x in ordered_dir_list if path.isdir(path.join(base_dir, x))] dir_list = [x for x in ordered_dir_list if path.isdir(path.join(base_dir, x))]
file_list = [x for x in ordered_dir_list if x not in dir_list] file_list = [x for x in ordered_dir_list if x not in dir_list]
if directory: if directory:
dir_list_obj = Directory.objects.filter(name__in=dir_list, dir_list_obj = Directory.objects.filter(name__in=dir_list, parent=directory)
parent=directory) file_list_obj = ComicBook.objects.filter(file_name__in=file_list, directory=directory)
file_list_obj = ComicBook.objects.filter(file_name__in=file_list,
directory=directory)
else: else:
dir_list_obj = Directory.objects.filter(name__in=dir_list, dir_list_obj = Directory.objects.filter(name__in=dir_list, parent__isnull=True)
parent__isnull=True) file_list_obj = ComicBook.objects.filter(file_name__in=file_list, directory__isnull=True)
file_list_obj = ComicBook.objects.filter(file_name__in=file_list,
directory__isnull=True)
for directory_obj in dir_list_obj: for directory_obj in dir_list_obj:
df = DirFile() df = DirFile()
df.populate_directory(directory_obj, user) df.populate_directory(directory_obj, user)
@@ -159,8 +154,7 @@ def generate_directory(user, directory=False):
file_list.remove(file_obj.file_name) file_list.remove(file_obj.file_name)
for directory_name in dir_list: for directory_name in dir_list:
if directory: if directory:
directory_obj = Directory(name=directory_name, directory_obj = Directory(name=directory_name, parent=directory)
parent=directory)
else: else:
directory_obj = Directory(name=directory_name) directory_obj = Directory(name=directory_name)
directory_obj.save() directory_obj.save()
@@ -168,7 +162,7 @@ def generate_directory(user, directory=False):
df.populate_directory(directory_obj, user) df.populate_directory(directory_obj, user)
files.append(df) files.append(df)
for file_name in file_list: for file_name in file_list:
if file_name.lower()[-4:] in ['.rar', '.zip', '.cbr', '.cbz']: if file_name.lower()[-4:] in [".rar", ".zip", ".cbr", ".cbz"]:
book = ComicBook.process_comic_book(file_name, directory) book = ComicBook.process_comic_book(file_name, directory)
df = DirFile() df = DirFile()
df.populate_comic(book, user) df.populate_comic(book, user)
@@ -184,17 +178,17 @@ def generate_label(book, status):
elif (status.last_read_page + 1) == book.page_count: elif (status.last_read_page + 1) == book.page_count:
label_text = '<center><span class="label label-success">Read</span></center>' label_text = '<center><span class="label label-success">Read</span></center>'
else: else:
label_text = '<center><span class="label label-primary">%s/%s</span></center>' % \ label_text = '<center><span class="label label-primary">%s/%s</span></center>' % (
(status.last_read_page + 1, book.page_count) status.last_read_page + 1,
book.page_count,
)
return label_text return label_text
def generate_dir_status(user, directory): def generate_dir_status(user, directory):
cb_list = ComicBook.objects.filter(directory=directory) cb_list = ComicBook.objects.filter(directory=directory)
total = cb_list.count() total = cb_list.count()
total_read = ComicStatus.objects.filter(user=user, total_read = ComicStatus.objects.filter(user=user, comic__in=cb_list, finished=True).count()
comic__in=cb_list,
finished=True).count()
if total == 0: if total == 0:
return '<center><span class="label label-default">Empty</span></center>' return '<center><span class="label label-default">Empty</span></center>'
elif total == total_read: elif total == total_read:

View File

@@ -18,8 +18,14 @@ from django.views.decorators.http import require_POST
from .forms import SettingsForm, AccountForm, EditUserForm, AddUserForm, InitialSetupForm from .forms import SettingsForm, AccountForm, EditUserForm, AddUserForm, InitialSetupForm
from .models import Setting, ComicBook, ComicStatus, Directory, ComicPage, UserMisc from .models import Setting, ComicBook, ComicStatus, Directory, ComicPage, UserMisc
from .util import generate_breadcrumbs_from_path, generate_breadcrumbs_from_menu, \ from .util import (
generate_title_from_path, Menu, generate_directory, generate_label generate_breadcrumbs_from_path,
generate_breadcrumbs_from_menu,
generate_title_from_path,
Menu,
generate_directory,
generate_label,
)
# noinspection PyTypeChecker # noinspection PyTypeChecker
@@ -27,11 +33,11 @@ from .util import generate_breadcrumbs_from_path, generate_breadcrumbs_from_menu
@login_required @login_required
def comic_list(request, directory_selector=False): def comic_list(request, directory_selector=False):
try: try:
base_dir = Setting.objects.get(name='BASE_DIR').value base_dir = Setting.objects.get(name="BASE_DIR").value
except Setting.DoesNotExist: except Setting.DoesNotExist:
return redirect('/comic/settings/') return redirect("/comic/settings/")
if not path.isdir(base_dir): if not path.isdir(base_dir):
return redirect('/comic/settings/') return redirect("/comic/settings/")
if directory_selector: if directory_selector:
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector)) selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
@@ -42,18 +48,17 @@ def comic_list(request, directory_selector=False):
if directory: if directory:
title = generate_title_from_path(directory.path) title = generate_title_from_path(directory.path)
breadcrumbs = generate_breadcrumbs_from_path(directory) breadcrumbs = generate_breadcrumbs_from_path(directory)
json_url = '/comic/list_json/{0}/'.format(directory_selector) json_url = "/comic/list_json/{0}/".format(directory_selector)
else: else:
title = generate_title_from_path('Home') title = generate_title_from_path("Home")
breadcrumbs = generate_breadcrumbs_from_path() breadcrumbs = generate_breadcrumbs_from_path()
json_url = '/comic/list_json/' json_url = "/comic/list_json/"
return render(request, 'comic/comic_list.html', { return render(
'breadcrumbs': breadcrumbs, request,
'menu': Menu(request.user, 'Browse'), "comic/comic_list.html",
'title': title, {"breadcrumbs": breadcrumbs, "menu": Menu(request.user, "Browse"), "title": title, "json_url": json_url},
'json_url': json_url )
})
@login_required @login_required
@@ -67,100 +72,99 @@ def comic_list_json(request, directory_selector=False):
directory = False directory = False
files = generate_directory(request.user, directory) files = generate_directory(request.user, directory)
response_data = dict() response_data = dict()
response_data['data'] = [] response_data["data"] = []
for file in files: for file in files:
response_data['data'].append({ response_data["data"].append(
'blank': '', {
'selector': file.selector, "blank": "",
'type': file.type, "selector": file.selector,
'icon': icon_str.format(file.icon), "type": file.type,
'name': file.name, "icon": icon_str.format(file.icon),
'label': file.label, "name": file.name,
'url': file.location, "label": file.label,
}) "url": file.location,
return HttpResponse( }
json.dumps(response_data),
content_type="application/json"
) )
return HttpResponse(json.dumps(response_data), content_type="application/json")
@login_required @login_required
def recent_comics(request): def recent_comics(request):
feed_id, _ = UserMisc.objects.get_or_create(user=request.user) feed_id, _ = UserMisc.objects.get_or_create(user=request.user)
return render(request, return render(
'comic/recent_comics.html', request,
"comic/recent_comics.html",
{ {
'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(feed_id.feed_id.bytes),
}) },
@login_required
@require_POST
def recent_comics_json(request):
start = int(request.POST['start'])
end = start + int(request.POST['length'])
icon = '<span class="glyphicon glyphicon-book"></span>'
comics = ComicBook.objects.all()
response_data = dict()
response_data['recordsTotal'] = comics.count()
if request.POST['search[value]']:
comics = comics.filter(file_name__contains=request.POST['search[value]'])
order_string = ''
# Ordering
if request.POST['order[0][dir]'] == 'desc':
order_string += '-'
if request.POST['order[0][dir]'] == '3':
order_string += 'date_added'
elif request.POST['order[0][dir]'] == '2':
order_string += 'date_added'
else:
order_string += 'date_added'
comics = comics.order_by(order_string)
response_data['recordsFiltered'] = comics.count()
response_data['data'] = list()
for book in comics[start:end]:
status, created = ComicStatus.objects.get_or_create(comic=book,
user=request.user)
if created:
status.save()
response_data['data'].append({
'selector': urlsafe_base64_encode(book.selector.bytes),
'icon': icon,
'type': 'book',
'name': book.file_name,
'date': book.date_added.strftime('%d/%m/%y-%H:%M'),
'label': generate_label(book, status),
'url': '/comic/read/{0}/{1}/'.format(urlsafe_base64_encode(book.selector.bytes),
status.last_read_page)
})
return HttpResponse(
json.dumps(response_data),
content_type="application/json"
) )
@login_required
@require_POST
def recent_comics_json(request):
start = int(request.POST["start"])
end = start + int(request.POST["length"])
icon = '<span class="glyphicon glyphicon-book"></span>'
comics = ComicBook.objects.all()
response_data = dict()
response_data["recordsTotal"] = comics.count()
if request.POST["search[value]"]:
comics = comics.filter(file_name__contains=request.POST["search[value]"])
order_string = ""
# Ordering
if request.POST["order[0][dir]"] == "desc":
order_string += "-"
if request.POST["order[0][dir]"] == "3":
order_string += "date_added"
elif request.POST["order[0][dir]"] == "2":
order_string += "date_added"
else:
order_string += "date_added"
comics = comics.order_by(order_string)
response_data["recordsFiltered"] = comics.count()
response_data["data"] = list()
for book in comics[start:end]:
status, created = ComicStatus.objects.get_or_create(comic=book, user=request.user)
if created:
status.save()
response_data["data"].append(
{
"selector": urlsafe_base64_encode(book.selector.bytes),
"icon": icon,
"type": "book",
"name": book.file_name,
"date": book.date_added.strftime("%d/%m/%y-%H:%M"),
"label": generate_label(book, status),
"url": "/comic/read/{0}/{1}/".format(
urlsafe_base64_encode(book.selector.bytes), status.last_read_page
),
}
)
return HttpResponse(json.dumps(response_data), content_type="application/json")
@login_required @login_required
@require_POST @require_POST
def comic_edit(request): def comic_edit(request):
if 'selected' not in request.POST: if "selected" not in request.POST:
return HttpResponse(status=200) return HttpResponse(status=200)
if request.POST['func'] == 'choose': if request.POST["func"] == "choose":
return HttpResponse(status=200) return HttpResponse(status=200)
selected = [uuid.UUID(bytes=urlsafe_base64_decode(item)) for item in request.POST.getlist('selected')] selected = [uuid.UUID(bytes=urlsafe_base64_decode(item)) for item in request.POST.getlist("selected")]
comics = ComicBook.objects.filter(selector__in=selected) comics = ComicBook.objects.filter(selector__in=selected)
with atomic(): with atomic():
for comic in comics: for comic in comics:
status, _ = ComicStatus.objects.get_or_create(comic=comic, status, _ = ComicStatus.objects.get_or_create(comic=comic, user=request.user)
user=request.user) if request.POST["func"] == "read":
if request.POST['func'] == 'read':
status.unread = False status.unread = False
status.finished = True status.finished = True
status.last_read_page = comic.page_count - 1 status.last_read_page = comic.page_count - 1
elif request.POST['func'] == 'unread': elif request.POST["func"] == "unread":
status.unread = True status.unread = True
status.finished = False status.finished = False
status.last_read_page = 0 status.last_read_page = 0
@@ -174,44 +178,37 @@ def account_page(request):
if request.POST: if request.POST:
form = AccountForm(request.POST) form = AccountForm(request.POST)
if form.is_valid(): if form.is_valid():
if form.cleaned_data['email'] != request.user.email: if form.cleaned_data["email"] != request.user.email:
request.user.email = form.cleaned_data['email'] request.user.email = form.cleaned_data["email"]
success_message.append('Email Updated.') success_message.append("Email Updated.")
if len(form.cleaned_data['password']) != 0: if len(form.cleaned_data["password"]) != 0:
request.user.set_password(form.cleaned_data['password']) request.user.set_password(form.cleaned_data["password"])
success_message.append('Password Updated.') success_message.append("Password Updated.")
request.user.save() request.user.save()
else: else:
form = AccountForm(initial={ form = AccountForm(initial={"username": request.user.username, "email": request.user.email})
'username': request.user.username, crumbs = [("Account", "/comic/account/")]
'email': request.user.email,
})
crumbs = [
('Account', '/comic/account/'),
]
context = { context = {
'form': form, "form": form,
'menu': Menu(request.user, 'Account'), "menu": Menu(request.user, "Account"),
'error_message': form.errors, "error_message": form.errors,
'success_message': '</br>'.join(success_message), "success_message": "</br>".join(success_message),
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs), "breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
'title': 'CBWebReader - Account', "title": "CBWebReader - Account",
} }
return render(request, 'comic/settings_page.html', context) return render(request, "comic/settings_page.html", context)
@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()
crumbs = [ crumbs = [("Users", "/comic/settings/users/")]
('Users', '/comic/settings/users/'),
]
context = { context = {
'users': users, "users": users,
'menu': Menu(request.user, 'Users'), "menu": Menu(request.user, "Users"),
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs), "breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
} }
return render(request, 'comic/users_page.html', context) return render(request, "comic/users_page.html", context)
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
@@ -221,89 +218,78 @@ def user_config_page(request, user_id):
if request.POST: if request.POST:
form = EditUserForm(request.POST) form = EditUserForm(request.POST)
if form.is_valid(): if form.is_valid():
if 'password' in form.cleaned_data: if "password" in form.cleaned_data:
if len(form.cleaned_data['password']) != 0: if len(form.cleaned_data["password"]) != 0:
user.set_password(form.cleaned_data['password']) user.set_password(form.cleaned_data["password"])
success_message.append('Password Updated.') success_message.append("Password Updated.")
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.save() user.save()
else: else:
form = EditUserForm(initial=EditUserForm.get_initial_values(user)) form = EditUserForm(initial=EditUserForm.get_initial_values(user))
users = User.objects.all() users = User.objects.all()
crumbs = [ crumbs = [("Users", "/comic/settings/users/"), (user.username, "/comic/settings/users/" + str(user.id))]
('Users', '/comic/settings/users/'),
(user.username, '/comic/settings/users/' + str(user.id)),
]
context = { context = {
'form': form, "form": form,
'users': users, "users": users,
'menu': Menu(request.user, 'Users'), "menu": Menu(request.user, "Users"),
'error_message': form.errors, "error_message": form.errors,
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs), "breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
'success_message': '</br>'.join(success_message), "success_message": "</br>".join(success_message),
'title': 'CBWebReader - Edit User - ' + user.username, "title": "CBWebReader - Edit User - " + user.username,
} }
return render(request, 'comic/settings_page.html', context) return render(request, "comic/settings_page.html", context)
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
def user_add_page(request): def user_add_page(request):
success_message = '' success_message = ""
if request.POST: if request.POST:
form = AddUserForm(request.POST) form = AddUserForm(request.POST)
if form.is_valid(): if form.is_valid():
user = User( user = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"])
username=form.cleaned_data['username'], user.set_password(form.cleaned_data["password"])
email=form.cleaned_data['email'],
)
user.set_password(form.cleaned_data['password'])
user.save() user.save()
UserMisc.objects.create(user=user) UserMisc.objects.create(user=user)
success_message = 'User {} created.'.format(user.username) success_message = "User {} created.".format(user.username)
else: else:
form = AddUserForm() form = AddUserForm()
crumbs = [ crumbs = [("Users", "/comic/settings/users/"), ("Add", "/comic/settings/users/add/")]
('Users', '/comic/settings/users/'),
('Add', '/comic/settings/users/add/'),
]
context = { context = {
'form': form, "form": form,
'menu': Menu(request.user, 'Users'), "menu": Menu(request.user, "Users"),
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs), "breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
'error_message': form.errors, "error_message": form.errors,
'success_message': success_message, "success_message": success_message,
'title': 'CBWebReader - Add User', "title": "CBWebReader - Add User",
} }
return render(request, 'comic/settings_page.html', context) return render(request, "comic/settings_page.html", context)
@user_passes_test(lambda u: u.is_superuser) @user_passes_test(lambda u: u.is_superuser)
def settings_page(request): def settings_page(request):
success_message = [] success_message = []
crumbs = [ crumbs = [("Settings", "/comic/settings/")]
('Settings', '/comic/settings/'),
]
if request.POST: if request.POST:
form = SettingsForm(request.POST) form = SettingsForm(request.POST)
if form.is_valid(): if form.is_valid():
base_dir = Setting.objects.get(name='BASE_DIR') base_dir = Setting.objects.get(name="BASE_DIR")
base_dir.value = form.cleaned_data['base_dir'] base_dir.value = form.cleaned_data["base_dir"]
base_dir.save() base_dir.save()
success_message.append('Settings updated.') success_message.append("Settings updated.")
form = SettingsForm(initial=SettingsForm.get_initial_values()) form = SettingsForm(initial=SettingsForm.get_initial_values())
context = { context = {
'error_message': form.errors, "error_message": form.errors,
'success_message': '</br>'.join(success_message), "success_message": "</br>".join(success_message),
'form': form, "form": form,
'menu': Menu(request.user, 'Settings'), "menu": Menu(request.user, "Settings"),
'title': 'CBWebReader - Settings', "title": "CBWebReader - Settings",
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs), "breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
} }
return render(request, 'comic/settings_page.html', context) return render(request, "comic/settings_page.html", context)
@login_required @login_required
@@ -317,21 +303,21 @@ def read_comic(request, comic_selector, page):
status, _ = ComicStatus.objects.get_or_create(comic=book, user=request.user) status, _ = ComicStatus.objects.get_or_create(comic=book, user=request.user)
status.unread = False status.unread = False
status.last_read_page = page status.last_read_page = page
if ComicPage.objects.filter(Comic=book).aggregate(Max('index'))['index__max'] == status.last_read_page: if ComicPage.objects.filter(Comic=book).aggregate(Max("index"))["index__max"] == status.last_read_page:
status.finished = True status.finished = True
else: else:
status.finished = False status.finished = False
status.save() status.save()
title = 'CBWebReader - ' + book.file_name + ' - Page: ' + str(page) title = "CBWebReader - " + book.file_name + " - Page: " + str(page)
context = { context = {
'book': book, "book": book,
'orig_file_name': book.page_name(page), "orig_file_name": book.page_name(page),
'nav': book.nav(page, request.user), "nav": book.nav(page, request.user),
'breadcrumbs': breadcrumbs, "breadcrumbs": breadcrumbs,
'menu': Menu(request.user), "menu": Menu(request.user),
'title': title, "title": title,
} }
return render(request, 'comic/read_comic.html', context) return render(request, "comic/read_comic.html", context)
@login_required @login_required
@@ -344,34 +330,29 @@ def get_image(_, comic_selector, page):
def initial_setup(request): def initial_setup(request):
if User.objects.all().exists(): if User.objects.all().exists():
return redirect('/comic/') return redirect("/comic/")
if request.POST: if request.POST:
form = InitialSetupForm(request.POST) form = InitialSetupForm(request.POST)
if form.is_valid(): if form.is_valid():
user = User( user = User(
username=form.cleaned_data['username'], username=form.cleaned_data["username"],
email=form.cleaned_data['email'], email=form.cleaned_data["email"],
is_staff=True, is_staff=True,
is_superuser=True, is_superuser=True,
) )
user.set_password(form.cleaned_data['password']) user.set_password(form.cleaned_data["password"])
user.save() user.save()
base_dir, _ = Setting.objects.get_or_create(name='BASE_DIR') base_dir, _ = Setting.objects.get_or_create(name="BASE_DIR")
base_dir.value = form.cleaned_data['base_dir'] base_dir.value = form.cleaned_data["base_dir"]
base_dir.save() base_dir.save()
user = authenticate(username=form.cleaned_data['username'], user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"])
password=form.cleaned_data['password'])
login(request, user) login(request, user)
return redirect('/comic/') return redirect("/comic/")
else: else:
form = InitialSetupForm() form = InitialSetupForm()
context = { context = {"form": form, "title": "CBWebReader - Setup", "error_message": form.errors}
'form': form, return render(request, "comic/settings_page.html", context)
'title': 'CBWebReader - Setup',
'error_message': form.errors,
}
return render(request, 'comic/settings_page.html', context)
def comic_redirect(_): def comic_redirect(_):
return redirect('/comic/') return redirect("/comic/")

View File

@@ -6,26 +6,19 @@ from snowpenguin.django.recaptcha2.widgets import ReCaptchaWidget
class LoginForm(forms.Form): class LoginForm(forms.Form):
username = forms.CharField(max_length=50, username = forms.CharField(
label='', max_length=50,
label="",
widget=forms.TextInput( widget=forms.TextInput(
attrs={ attrs={"class": "form-control", "placeholder": "Username", "autofocus": True, "required": True}
'class': 'form-control', ),
'placeholder': 'Username', )
'autofocus': True, password = forms.CharField(
'required': True, label="Password",
} widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "Username", "required": True}),
)) )
password = forms.CharField(label='Password',
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
'placeholder': 'Username',
'required': True,
}
))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
if settings.CBREADER_USE_RECAPTCHA if hasattr(settings, 'CBREADER_USE_RECAPTCHA') else False: if settings.CBREADER_USE_RECAPTCHA if hasattr(settings, "CBREADER_USE_RECAPTCHA") else False:
self.fields['captcha'] = ReCaptchaField(widget=ReCaptchaWidget()) self.fields["captcha"] = ReCaptchaField(widget=ReCaptchaWidget())

View File

@@ -9,45 +9,28 @@ def comic_login(request):
if request.POST: if request.POST:
form = LoginForm(request.POST) form = LoginForm(request.POST)
if form.is_valid(): if form.is_valid():
user = authenticate(username=form.cleaned_data['username'], user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"])
password=form.cleaned_data['password'])
if user is not None: if user is not None:
if user.is_active: if user.is_active:
login(request, user) login(request, user)
if 'next' in request.GET: if "next" in request.GET:
return redirect(request.GET['next']) return redirect(request.GET["next"])
else: else:
return redirect('/comic/') return redirect("/comic/")
else: else:
return render(request, return render(request, "comic_auth/login.html", {"error": True})
'comic_auth/login.html',
{
'error': True,
})
else: else:
return render(request, return render(request, "comic_auth/login.html", {"error": True, "form": form})
'comic_auth/login.html',
{
'error': True,
'form': form
})
else: else:
return render(request, return render(request, "comic_auth/login.html", {"error": True, "form": form})
'comic_auth/login.html',
{
'error': True,
'form': form
})
else: else:
if not User.objects.all().exists(): if not User.objects.all().exists():
return redirect('/setup/') return redirect("/setup/")
form = LoginForm() form = LoginForm()
context = { context = {"form": form}
'form': form return render(request, "comic_auth/login.html", context)
}
return render(request, 'comic_auth/login.html', context)
def comic_logout(request): def comic_logout(request):
logout(request) logout(request)
return redirect('/login/') return redirect("/login/")

View File

@@ -14,11 +14,11 @@ services:
- database - database
ports: ports:
- "8000:8000" - "8000:8000"
volumes: # volumes:
- ./cbreader:/src/cbreader # - ./cbreader:/src/cbreader
- ./comic:/src/comic # - ./comic:/src/comic
- ./comic_auth:/src/comic_auth # - ./comic_auth:/src/comic_auth
- ${COMIC_BOOK_VOLUME}:/data # - ${COMIC_BOOK_VOLUME}:/data
command: python manage.py runserver 0.0.0.0:8000 command: python manage.py runserver 0.0.0.0:8000
database: database:

2
pyproject.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line_length = 119

View File

@@ -1,12 +1,13 @@
from distutils.core import setup from distutils.core import setup
setup( setup(
name='cbwebreader', name="cbwebreader",
version='', version="",
packages=['comic', 'comic.migrations', 'cbreader', 'comic_auth', 'comic_auth.migrations'], packages=["comic", "comic.migrations", "cbreader", "comic_auth", "comic_auth.migrations"],
url='https://github.com/ajurna/cbwebreader', url="https://github.com/ajurna/cbwebreader",
license='http://creativecommons.org/licenses/by-sa/4.0/', license="http://creativecommons.org/licenses/by-sa/4.0/",
author='Ajurna', author="Ajurna",
author_email='ajurna@gmail.com', author_email="ajurna@gmail.com",
description='Comic Book Web Reader', requires=['django-recaptcha', 'django', 'ujson'] description="Comic Book Web Reader",
requires=["django-recaptcha", "django", "ujson"],
) )