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/
# 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!
DEBUG = os.environ.get('DJANGO_DEBUG', True)
DEBUG = os.environ.get("DJANGO_DEBUG", True)
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
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',
"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',
"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'
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',
],
},
"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'
WSGI_APPLICATION = "cbreader.wsgi.application"
# Database
@@ -83,20 +83,15 @@ DATABASE_URL = os.getenv("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'),
}
}
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'
LANGUAGE_CODE = "en-ie"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@@ -108,16 +103,16 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# 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)
CBREADER_USE_RECAPTCHA = False
RECAPTCHA_PRIVATE_KEY = ''
RECAPTCHA_PUBLIC_KEY = ''
RECAPTCHA_PRIVATE_KEY = ""
RECAPTCHA_PUBLIC_KEY = ""
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
urlpatterns = [
url(r'^$', comic.views.comic_redirect),
url(r'^login/', comic_auth.views.comic_login),
url(r'^logout/', comic_auth.views.comic_logout),
url(r'^setup/', comic.views.initial_setup),
url(r'^comic/', include('comic.urls')),
url(r'^admin/', admin.site.urls),
url(r"^$", comic.views.comic_redirect),
url(r"^login/", comic_auth.views.comic_login),
url(r"^logout/", comic_auth.views.comic_logout),
url(r"^setup/", comic.views.initial_setup),
url(r"^comic/", include("comic.urls")),
url(r"^admin/", admin.site.urls),
# 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)
class SettingAdmin(admin.ModelAdmin):
list_display = ('name', 'value')
list_display = ("name", "value")
@admin.register(ComicBook)
class ComicBookAdmin(admin.ModelAdmin):
list_display = ['file_name', 'date_added']
search_fields = ['file_name']
list_display = ["file_name", "date_added"]
search_fields = ["file_name"]
@admin.register(ComicPage)
class ComicPageAdmin(admin.ModelAdmin):
list_display = ('Comic', 'index', 'page_file_name', 'content_type')
list_filter = ['Comic']
list_display = ("Comic", "index", "page_file_name", "content_type")
list_filter = ["Comic"]
@admin.register(ComicStatus)
class ComicStatusAdmin(admin.ModelAdmin):
list_display = ['user', 'comic', 'last_read_page', 'unread']
list_display = ["user", "comic", "last_read_page", "unread"]
@admin.register(Directory)

View File

@@ -19,14 +19,14 @@ class RecentComics(Feed):
@staticmethod
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:
return item.file_name
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.
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):
username = forms.CharField(help_text='Username',
widget=forms.TextInput(
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',
}
))
base_dir = forms.CharField(help_text='Base Directory',
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
username = forms.CharField(help_text="Username", widget=forms.TextInput(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"})
)
base_dir = forms.CharField(help_text="Base Directory", widget=forms.TextInput(attrs={"class": "form-control"}))
def clean_base_dir(self):
data = self.cleaned_data['base_dir']
data = self.cleaned_data["base_dir"]
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
def clean(self):
form_data = self.cleaned_data
if form_data['password'] != form_data['password_confirm']:
raise forms.ValidationError('Passwords do not match.')
if len(form_data['password']) < 8:
raise forms.ValidationError('Password is too short')
if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError("Passwords do not match.")
if len(form_data["password"]) < 8:
raise forms.ValidationError("Password is too short")
return form_data
class AccountForm(forms.Form):
username = forms.CharField(help_text='Username',
username = forms.CharField(
help_text="Username",
required=False,
widget=forms.TextInput(
attrs={
'class': 'form-control disabled',
'readonly': True,
}
))
email = forms.CharField(help_text='Email Address',
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
password = forms.CharField(help_text='New Password',
widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
)
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"})
)
password_confirm = forms.CharField(
help_text="New Password Confirmation",
required=False,
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
password_confirm = forms.CharField(help_text='New Password Confirmation',
required=False,
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
}
))
widget=forms.PasswordInput(attrs={"class": "form-control"}),
)
def clean_email(self):
data = self.cleaned_data['email']
user = User.objects.get(username=self.cleaned_data['username'])
data = self.cleaned_data["email"]
user = User.objects.get(username=self.cleaned_data["username"])
if data == user.email:
return data
if User.objects.filter(email=data).exists():
raise forms.ValidationError('Email Address is in use')
raise forms.ValidationError("Email Address is in use")
return data
def clean(self):
form_data = self.cleaned_data
if form_data['password'] != form_data['password_confirm']:
raise forms.ValidationError('Passwords do not match.')
if len(form_data['password']) < 8 & len(form_data['password']) != 0:
raise forms.ValidationError('Password is too short')
if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError("Passwords do not match.")
if len(form_data["password"]) < 8 & len(form_data["password"]) != 0:
raise forms.ValidationError("Password is too short")
return form_data
class AddUserForm(forms.Form):
username = forms.CharField(help_text='Username',
widget=forms.TextInput(
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',
}
))
username = forms.CharField(help_text="Username", widget=forms.TextInput(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):
data = self.cleaned_data['username']
data = self.cleaned_data["username"]
if User.objects.filter(username=data).exists():
raise forms.ValidationError('This username Exists.')
raise forms.ValidationError("This username Exists.")
return data
def clean_email(self):
data = self.cleaned_data['email']
data = self.cleaned_data["email"]
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
def clean(self):
form_data = self.cleaned_data
if form_data['password'] != form_data['password_confirm']:
raise forms.ValidationError('Passwords do not match.')
if len(form_data['password']) < 8:
raise forms.ValidationError('Password is too short')
if form_data["password"] != form_data["password_confirm"]:
raise forms.ValidationError("Passwords do not match.")
if len(form_data["password"]) < 8:
raise forms.ValidationError("Password is too short")
return form_data
class EditUserForm(forms.Form):
username = forms.CharField(help_text='Username',
username = forms.CharField(
help_text="Username",
required=False,
widget=forms.TextInput(
attrs={
'class': 'form-control disabled',
'readonly': True,
}
))
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',
}
))
widget=forms.TextInput(attrs={"class": "form-control disabled", "readonly": True}),
)
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
@staticmethod
def get_initial_values(user):
out = {
'username': user.username,
'email': user.email
}
out = {"username": user.username, "email": user.email}
return out
def clean_email(self):
data = self.cleaned_data['email']
user = User.objects.get(username=self.cleaned_data['username'])
data = self.cleaned_data["email"]
user = User.objects.get(username=self.cleaned_data["username"])
if data == user.email:
return data
if User.objects.filter(email=data).exists():
raise forms.ValidationError('Email Address is in use')
raise forms.ValidationError("Email Address is in use")
return data
def clean_password(self):
data = self.cleaned_data['password']
data = self.cleaned_data["password"]
if len(data) < 8 & len(data) != 0:
raise forms.ValidationError('Password is too short')
raise forms.ValidationError("Password is too short")
return data
class SettingsForm(forms.Form):
base_dir = forms.CharField(help_text='Base Directory',
widget=forms.TextInput(
attrs={
'class': 'form-control'
}
))
base_dir = forms.CharField(help_text="Base Directory", widget=forms.TextInput(attrs={"class": "form-control"}))
def clean_base_dir(self):
data = self.cleaned_data['base_dir']
data = self.cleaned_data["base_dir"]
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
@staticmethod
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 = {
'base_dir': base_dir.value,
}
initial = {"base_dir": base_dir.value}
return initial

View File

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

View File

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

View File

@@ -6,14 +6,8 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('comic', '0001_initial'),
]
dependencies = [("comic", "0001_initial")]
operations = [
migrations.AlterField(
model_name='setting',
name='name',
field=models.CharField(unique=True, max_length=50),
),
migrations.AlterField(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):
dependencies = [
('comic', '0002_auto_20150616_1613'),
]
dependencies = [("comic", "0002_auto_20150616_1613")]
operations = [
migrations.CreateModel(
name='ComicBook',
name="ComicBook",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('file_name', models.CharField(unique=True, max_length=100)),
('last_read_page', models.IntegerField()),
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
("file_name", models.CharField(unique=True, max_length=100)),
("last_read_page", models.IntegerField()),
],
),
migrations.CreateModel(
name='ComicPage',
name="ComicPage",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('index', models.IntegerField()),
('page_file_name', models.CharField(max_length=100)),
('content_type', models.CharField(max_length=30)),
('Comic', models.ForeignKey(to='comic.ComicBook', on_delete=models.CASCADE)),
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
("index", models.IntegerField()),
("page_file_name", models.CharField(max_length=100)),
("content_type", models.CharField(max_length=30)),
("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):
dependencies = [
('comic', '0003_comicbook_comicpage'),
]
dependencies = [("comic", "0003_comicbook_comicpage")]
operations = [
migrations.AddField(
model_name='comicbook',
name='unread',
field=models.BooleanField(default=True),
preserve_default=False,
),
model_name="comicbook", 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):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comic', '0004_comicbook_unread'),
]
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("comic", "0004_comicbook_unread")]
operations = [
migrations.CreateModel(
name='ComicStatus',
name="ComicStatus",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('last_read_page', models.IntegerField()),
('unread', models.BooleanField()),
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
("last_read_page", models.IntegerField()),
("unread", models.BooleanField()),
],
),
migrations.RemoveField(
model_name='comicbook',
name='last_read_page',
),
migrations.RemoveField(
model_name='comicbook',
name='unread',
migrations.RemoveField(model_name="comicbook", name="last_read_page"),
migrations.RemoveField(model_name="comicbook", name="unread"),
migrations.AddField(
model_name="comicstatus",
name="comic",
field=models.ForeignKey(to="comic.ComicBook", on_delete=models.CASCADE),
),
migrations.AddField(
model_name='comicstatus',
name='comic',
field=models.ForeignKey(to='comic.ComicBook', on_delete=models.CASCADE),
),
migrations.AddField(
model_name='comicstatus',
name='user',
model_name="comicstatus",
name="user",
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):
dependencies = [
('comic', '0005_auto_20150625_1400'),
]
dependencies = [("comic", "0005_auto_20150625_1400")]
operations = [
migrations.AlterField(
model_name='comicstatus',
name='last_read_page',
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name='comicstatus',
name='unread',
field=models.BooleanField(default=True),
),
migrations.AlterField(model_name="comicstatus", 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):
dependencies = [
('comic', '0006_auto_20150625_1411'),
]
dependencies = [("comic", "0006_auto_20150625_1411")]
operations = [
migrations.AlterField(
model_name='setting',
name='name',
field=models.CharField(unique=True, max_length=100),
),
migrations.AlterField(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):
dependencies = [
('comic', '0007_auto_20150626_1820'),
]
dependencies = [("comic", "0007_auto_20150626_1820")]
operations = [
migrations.CreateModel(
name='Directory',
name="Directory",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('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')),
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=100)),
("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"
),
),
],
),
migrations.AddField(
model_name='comicbook',
name='date_added',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 31, 10, 40, 30, 62170, tzinfo=utc)),
model_name="comicbook",
name="date_added",
field=models.DateTimeField(
auto_now_add=True, default=datetime.datetime(2016, 3, 31, 10, 40, 30, 62170, tzinfo=utc)
),
preserve_default=False,
),
migrations.AddField(
model_name='comicbook',
name='selector',
field=models.UUIDField(default=uuid.uuid4, null=True),
model_name="comicbook", name="selector", field=models.UUIDField(default=uuid.uuid4, null=True)
),
migrations.AddField(model_name="comicbook", name="version", field=models.IntegerField(default=0)),
migrations.AddField(
model_name='comicbook',
name='version',
field=models.IntegerField(default=0),
model_name="comicbook",
name="directory",
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):
comicbook = apps.get_model('comic', 'comicbook')
comicbook = apps.get_model("comic", "comicbook")
for row in comicbook.objects.all():
row.selector = uuid.uuid4()
row.save()
directory = apps.get_model('comic', 'directory')
directory = apps.get_model("comic", "directory")
for row in directory.objects.all():
row.selector = uuid.uuid4()
row.save()
@@ -20,10 +20,6 @@ def gen_uuid(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('comic', '0008_auto_20160331_1140'),
]
dependencies = [("comic", "0008_auto_20160331_1140")]
operations = [
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
]
operations = [migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop)]

View File

@@ -9,19 +9,13 @@ import uuid
class Migration(migrations.Migration):
dependencies = [
('comic', '0009_auto_20160331_1140'),
]
dependencies = [("comic", "0009_auto_20160331_1140")]
operations = [
migrations.AlterField(
model_name='comicbook',
name='selector',
field=models.UUIDField(default=uuid.uuid4, unique=True),
model_name="comicbook", name="selector", field=models.UUIDField(default=uuid.uuid4, unique=True)
),
migrations.AlterField(
model_name='directory',
name='selector',
field=models.UUIDField(default=uuid.uuid4, unique=True),
model_name="directory", 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):
dependencies = [
('comic', '0010_auto_20160331_1140'),
]
dependencies = [("comic", "0010_auto_20160331_1140")]
operations = [
migrations.AlterField(
model_name='comicbook',
name='version',
field=models.IntegerField(default=1),
),
]
operations = [migrations.AlterField(model_name="comicbook", name="version", field=models.IntegerField(default=1))]

View File

@@ -8,19 +8,17 @@ import uuid
class Migration(migrations.Migration):
dependencies = [
('comic', '0011_auto_20160331_1141'),
]
dependencies = [("comic", "0011_auto_20160331_1141")]
operations = [
migrations.AlterField(
model_name='comicbook',
name='selector',
model_name="comicbook",
name="selector",
field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
),
migrations.AlterField(
model_name='directory',
name='selector',
model_name="directory",
name="selector",
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):
dependencies = [
('comic', '0012_auto_20160401_0949'),
]
dependencies = [("comic", "0012_auto_20160401_0949")]
operations = [
migrations.AddField(
model_name='comicstatus',
name='finished',
field=models.BooleanField(default=False),
),
migrations.AddField(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.models import Max
def set_finished(apps, schema_editor):
comicstatus = apps.get_model('comic', 'comicstatus')
comicpage = apps.get_model('comic', 'ComicPage')
comicstatus = apps.get_model("comic", "comicstatus")
comicpage = apps.get_model("comic", "ComicPage")
for row in comicstatus.objects.all():
last_page = comicpage.objects.filter(Comic=row.comic).aggregate(Max('index'))
if row.last_read_page == last_page['index__max']:
last_page = comicpage.objects.filter(Comic=row.comic).aggregate(Max("index"))
if row.last_read_page == last_page["index__max"]:
row.finished = True
row.save()
class Migration(migrations.Migration):
dependencies = [
('comic', '0013_comicstatus_finished'),
]
dependencies = [("comic", "0013_comicstatus_finished")]
operations = [
migrations.RunPython(set_finished, reverse_code=migrations.RunPython.noop),
]
operations = [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):
dependencies = [
('comic', '0014_auto_20160404_1402'),
]
dependencies = [("comic", "0014_auto_20160404_1402")]
operations = [
migrations.AlterField(
model_name='comicbook',
name='file_name',
field=models.CharField(max_length=100),
),
migrations.AlterField(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):
dependencies = [
('comic', '0015_auto_20160405_1126'),
]
dependencies = [("comic", "0015_auto_20160405_1126")]
operations = [
migrations.AlterField(
model_name='comicpage',
name='page_file_name',
field=models.CharField(max_length=200),
),
migrations.AlterField(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):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('comic', '0016_auto_20160414_1335'),
]
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("comic", "0016_auto_20160414_1335")]
operations = [
migrations.CreateModel(
name='UserMisc',
name="UserMisc",
fields=[
('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)),
("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)),
(
'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):
user_misc = apps.get_model('comic', 'UserMisc')
User = apps.get_model('auth', 'user')
user_misc = apps.get_model("comic", "UserMisc")
User = apps.get_model("auth", "user")
for user in User.objects.all():
um = user_misc.objects.create(user=user)
class Migration(migrations.Migration):
dependencies = [
('comic', '0017_usermisc'),
]
dependencies = [("comic", "0017_usermisc")]
operations = [
migrations.RunPython(gen_feeds, reverse_code=migrations.RunPython.noop),
]
operations = [migrations.RunPython(gen_feeds, reverse_code=migrations.RunPython.noop)]

View File

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

View File

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

View File

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

View File

@@ -2,20 +2,21 @@ from django.conf.urls import url
from . import feeds
from . import views
urlpatterns = [
url(r'^$', views.comic_list, name='index'),
url(r'^settings/$', views.settings_page, name='settings'),
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/add/$', views.user_add_page, name='users'),
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]+)/img$', views.get_image, name='get_image'),
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'^recent/$', views.recent_comics, name='recent_comics'),
url(r'^recent/json/$', views.recent_comics_json, name='recent_comics_json'),
url(r'^edit/$', views.comic_edit, name='comic_edit'),
url(r'^feed/(?P<user_selector>[\w-]+)/$', feeds.RecentComics()),
url(r'^(?P<directory_selector>[\w-]+)/$', views.comic_list, name='comic_list'),
url(r"^$", views.comic_list, name="index"),
url(r"^settings/$", views.settings_page, name="settings"),
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/add/$", views.user_add_page, name="users"),
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]+)/img$", views.get_image, name="get_image"),
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"^recent/$", views.recent_comics, name="recent_comics"),
url(r"^recent/json/$", views.recent_comics_json, name="recent_comics_json"),
url(r"^edit/$", views.comic_edit, name="comic_edit"),
url(r"^feed/(?P<user_selector>[\w-]+)/$", feeds.RecentComics()),
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):
if file_path == '':
return 'CBWebReader'
return 'CBWebReader - ' + ' - '.join(file_path.split(path.sep))
if file_path == "":
return "CBWebReader"
return "CBWebReader - " + " - ".join(file_path.split(path.sep))
class Menu:
def __init__(self, user, page=''):
def __init__(self, user, page=""):
"""
:type page: str
"""
self.menu_items = OrderedDict()
self.menu_items['Browse'] = '/comic/'
self.menu_items['Recent'] = '/comic/recent/'
self.menu_items['Account'] = '/comic/account/'
self.menu_items["Browse"] = "/comic/"
self.menu_items["Recent"] = "/comic/recent/"
self.menu_items["Account"] = "/comic/account/"
if user.is_superuser:
self.menu_items['Settings'] = '/comic/settings/'
self.menu_items['Users'] = '/comic/settings/users/'
self.menu_items['Logout'] = '/logout/'
self.menu_items["Settings"] = "/comic/settings/"
self.menu_items["Users"] = "/comic/settings/users/"
self.menu_items["Logout"] = "/logout/"
self.current_page = page
class Breadcrumb:
def __init__(self):
self.name = 'Home'
self.url = '/comic/'
self.name = "Home"
self.url = "/comic/"
def __str__(self):
return self.name
@@ -56,12 +56,12 @@ def generate_breadcrumbs_from_path(directory=False, book=False):
for item in folders[::-1]:
bc = Breadcrumb()
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)
if book:
bc = Breadcrumb()
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)
return output
@@ -79,46 +79,45 @@ def generate_breadcrumbs_from_menu(paths):
class DirFile:
def __init__(self):
self.name = ''
self.icon = ''
self.location = ''
self.label = ''
self.type = ''
self.selector = ''
self.name = ""
self.icon = ""
self.location = ""
self.label = ""
self.type = ""
self.selector = ""
def __str__(self):
return self.name
def populate_directory(self, directory, user):
self.name = directory.name
self.icon = 'glyphicon-folder-open'
self.icon = "glyphicon-folder-open"
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.type = 'directory'
self.type = "directory"
def populate_comic(self, comic, user):
if type(comic) == str:
self.icon = 'glyphicon-remove'
self.icon = "glyphicon-remove"
self.name = comic
self.selector = '0'
self.location = '/'
self.selector = "0"
self.location = "/"
self.label = '<center><span class="label label-danger">Error</span></center>'
self.type = 'book'
self.type = "book"
else:
self.icon = 'glyphicon-book'
self.icon = "glyphicon-book"
self.name = comic.file_name
status, created = ComicStatus.objects.get_or_create(comic=comic, user=user)
if created:
status.save()
self.selector = urlsafe_base64_encode(comic.selector.bytes)
self.location = '/comic/read/{0}/{1}/'.format(self.selector,
status.last_read_page)
self.location = "/comic/read/{0}/{1}/".format(self.selector, status.last_read_page)
self.label = generate_label(comic, status)
self.type = 'book'
self.type = "book"
def __repr__(self):
return f'<DirFile: {self.name}: {self.type}>'
return f"<DirFile: {self.name}: {self.type}>"
def generate_directory(user, directory=False):
@@ -126,7 +125,7 @@ def generate_directory(user, directory=False):
:type user: User
:type directory: Directory
"""
base_dir = Setting.objects.get(name='BASE_DIR').value
base_dir = Setting.objects.get(name="BASE_DIR").value
files = []
if directory:
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))]
file_list = [x for x in ordered_dir_list if x not in dir_list]
if directory:
dir_list_obj = Directory.objects.filter(name__in=dir_list,
parent=directory)
file_list_obj = ComicBook.objects.filter(file_name__in=file_list,
directory=directory)
dir_list_obj = Directory.objects.filter(name__in=dir_list, parent=directory)
file_list_obj = ComicBook.objects.filter(file_name__in=file_list, directory=directory)
else:
dir_list_obj = Directory.objects.filter(name__in=dir_list,
parent__isnull=True)
file_list_obj = ComicBook.objects.filter(file_name__in=file_list,
directory__isnull=True)
dir_list_obj = Directory.objects.filter(name__in=dir_list, parent__isnull=True)
file_list_obj = ComicBook.objects.filter(file_name__in=file_list, directory__isnull=True)
for directory_obj in dir_list_obj:
df = DirFile()
df.populate_directory(directory_obj, user)
@@ -159,8 +154,7 @@ def generate_directory(user, directory=False):
file_list.remove(file_obj.file_name)
for directory_name in dir_list:
if directory:
directory_obj = Directory(name=directory_name,
parent=directory)
directory_obj = Directory(name=directory_name, parent=directory)
else:
directory_obj = Directory(name=directory_name)
directory_obj.save()
@@ -168,7 +162,7 @@ def generate_directory(user, directory=False):
df.populate_directory(directory_obj, user)
files.append(df)
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)
df = DirFile()
df.populate_comic(book, user)
@@ -184,17 +178,17 @@ def generate_label(book, status):
elif (status.last_read_page + 1) == book.page_count:
label_text = '<center><span class="label label-success">Read</span></center>'
else:
label_text = '<center><span class="label label-primary">%s/%s</span></center>' % \
(status.last_read_page + 1, book.page_count)
label_text = '<center><span class="label label-primary">%s/%s</span></center>' % (
status.last_read_page + 1,
book.page_count,
)
return label_text
def generate_dir_status(user, directory):
cb_list = ComicBook.objects.filter(directory=directory)
total = cb_list.count()
total_read = ComicStatus.objects.filter(user=user,
comic__in=cb_list,
finished=True).count()
total_read = ComicStatus.objects.filter(user=user, comic__in=cb_list, finished=True).count()
if total == 0:
return '<center><span class="label label-default">Empty</span></center>'
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 .models import Setting, ComicBook, ComicStatus, Directory, ComicPage, UserMisc
from .util import generate_breadcrumbs_from_path, generate_breadcrumbs_from_menu, \
generate_title_from_path, Menu, generate_directory, generate_label
from .util import (
generate_breadcrumbs_from_path,
generate_breadcrumbs_from_menu,
generate_title_from_path,
Menu,
generate_directory,
generate_label,
)
# noinspection PyTypeChecker
@@ -27,11 +33,11 @@ from .util import generate_breadcrumbs_from_path, generate_breadcrumbs_from_menu
@login_required
def comic_list(request, directory_selector=False):
try:
base_dir = Setting.objects.get(name='BASE_DIR').value
base_dir = Setting.objects.get(name="BASE_DIR").value
except Setting.DoesNotExist:
return redirect('/comic/settings/')
return redirect("/comic/settings/")
if not path.isdir(base_dir):
return redirect('/comic/settings/')
return redirect("/comic/settings/")
if directory_selector:
selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector))
@@ -42,18 +48,17 @@ def comic_list(request, directory_selector=False):
if directory:
title = generate_title_from_path(directory.path)
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:
title = generate_title_from_path('Home')
title = generate_title_from_path("Home")
breadcrumbs = generate_breadcrumbs_from_path()
json_url = '/comic/list_json/'
json_url = "/comic/list_json/"
return render(request, 'comic/comic_list.html', {
'breadcrumbs': breadcrumbs,
'menu': Menu(request.user, 'Browse'),
'title': title,
'json_url': json_url
})
return render(
request,
"comic/comic_list.html",
{"breadcrumbs": breadcrumbs, "menu": Menu(request.user, "Browse"), "title": title, "json_url": json_url},
)
@login_required
@@ -67,100 +72,99 @@ def comic_list_json(request, directory_selector=False):
directory = False
files = generate_directory(request.user, directory)
response_data = dict()
response_data['data'] = []
response_data["data"] = []
for file in files:
response_data['data'].append({
'blank': '',
'selector': file.selector,
'type': file.type,
'icon': icon_str.format(file.icon),
'name': file.name,
'label': file.label,
'url': file.location,
})
return HttpResponse(
json.dumps(response_data),
content_type="application/json"
response_data["data"].append(
{
"blank": "",
"selector": file.selector,
"type": file.type,
"icon": icon_str.format(file.icon),
"name": file.name,
"label": file.label,
"url": file.location,
}
)
return HttpResponse(json.dumps(response_data), content_type="application/json")
@login_required
def recent_comics(request):
feed_id, _ = UserMisc.objects.get_or_create(user=request.user)
return render(request,
'comic/recent_comics.html',
return render(
request,
"comic/recent_comics.html",
{
'breadcrumbs': generate_breadcrumbs_from_menu([('Recent', '/comic/recent/')]),
'menu': Menu(request.user, 'Recent'),
'title': 'Recent Comics',
'feed_id': urlsafe_base64_encode(feed_id.feed_id.bytes),
})
@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"
"breadcrumbs": generate_breadcrumbs_from_menu([("Recent", "/comic/recent/")]),
"menu": Menu(request.user, "Recent"),
"title": "Recent Comics",
"feed_id": urlsafe_base64_encode(feed_id.feed_id.bytes),
},
)
@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 comic_edit(request):
if 'selected' not in request.POST:
if "selected" not in request.POST:
return HttpResponse(status=200)
if request.POST['func'] == 'choose':
if request.POST["func"] == "choose":
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)
with atomic():
for comic in comics:
status, _ = ComicStatus.objects.get_or_create(comic=comic,
user=request.user)
if request.POST['func'] == 'read':
status, _ = ComicStatus.objects.get_or_create(comic=comic, user=request.user)
if request.POST["func"] == "read":
status.unread = False
status.finished = True
status.last_read_page = comic.page_count - 1
elif request.POST['func'] == 'unread':
elif request.POST["func"] == "unread":
status.unread = True
status.finished = False
status.last_read_page = 0
@@ -174,44 +178,37 @@ def account_page(request):
if request.POST:
form = AccountForm(request.POST)
if form.is_valid():
if form.cleaned_data['email'] != request.user.email:
request.user.email = form.cleaned_data['email']
success_message.append('Email Updated.')
if len(form.cleaned_data['password']) != 0:
request.user.set_password(form.cleaned_data['password'])
success_message.append('Password Updated.')
if form.cleaned_data["email"] != request.user.email:
request.user.email = form.cleaned_data["email"]
success_message.append("Email Updated.")
if len(form.cleaned_data["password"]) != 0:
request.user.set_password(form.cleaned_data["password"])
success_message.append("Password Updated.")
request.user.save()
else:
form = AccountForm(initial={
'username': request.user.username,
'email': request.user.email,
})
crumbs = [
('Account', '/comic/account/'),
]
form = AccountForm(initial={"username": request.user.username, "email": request.user.email})
crumbs = [("Account", "/comic/account/")]
context = {
'form': form,
'menu': Menu(request.user, 'Account'),
'error_message': form.errors,
'success_message': '</br>'.join(success_message),
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs),
'title': 'CBWebReader - Account',
"form": form,
"menu": Menu(request.user, "Account"),
"error_message": form.errors,
"success_message": "</br>".join(success_message),
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
"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)
def users_page(request):
users = User.objects.all()
crumbs = [
('Users', '/comic/settings/users/'),
]
crumbs = [("Users", "/comic/settings/users/")]
context = {
'users': users,
'menu': Menu(request.user, 'Users'),
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs),
"users": users,
"menu": Menu(request.user, "Users"),
"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)
@@ -221,89 +218,78 @@ def user_config_page(request, user_id):
if request.POST:
form = EditUserForm(request.POST)
if form.is_valid():
if 'password' in form.cleaned_data:
if len(form.cleaned_data['password']) != 0:
user.set_password(form.cleaned_data['password'])
success_message.append('Password Updated.')
if form.cleaned_data['email'] != user.email:
user.email = form.cleaned_data['email']
success_message.append('Email Updated.</br>')
if "password" in form.cleaned_data:
if len(form.cleaned_data["password"]) != 0:
user.set_password(form.cleaned_data["password"])
success_message.append("Password Updated.")
if form.cleaned_data["email"] != user.email:
user.email = form.cleaned_data["email"]
success_message.append("Email Updated.</br>")
user.save()
else:
form = EditUserForm(initial=EditUserForm.get_initial_values(user))
users = User.objects.all()
crumbs = [
('Users', '/comic/settings/users/'),
(user.username, '/comic/settings/users/' + str(user.id)),
]
crumbs = [("Users", "/comic/settings/users/"), (user.username, "/comic/settings/users/" + str(user.id))]
context = {
'form': form,
'users': users,
'menu': Menu(request.user, 'Users'),
'error_message': form.errors,
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs),
'success_message': '</br>'.join(success_message),
'title': 'CBWebReader - Edit User - ' + user.username,
"form": form,
"users": users,
"menu": Menu(request.user, "Users"),
"error_message": form.errors,
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
"success_message": "</br>".join(success_message),
"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)
def user_add_page(request):
success_message = ''
success_message = ""
if request.POST:
form = AddUserForm(request.POST)
if form.is_valid():
user = User(
username=form.cleaned_data['username'],
email=form.cleaned_data['email'],
)
user.set_password(form.cleaned_data['password'])
user = User(username=form.cleaned_data["username"], email=form.cleaned_data["email"])
user.set_password(form.cleaned_data["password"])
user.save()
UserMisc.objects.create(user=user)
success_message = 'User {} created.'.format(user.username)
success_message = "User {} created.".format(user.username)
else:
form = AddUserForm()
crumbs = [
('Users', '/comic/settings/users/'),
('Add', '/comic/settings/users/add/'),
]
crumbs = [("Users", "/comic/settings/users/"), ("Add", "/comic/settings/users/add/")]
context = {
'form': form,
'menu': Menu(request.user, 'Users'),
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs),
'error_message': form.errors,
'success_message': success_message,
'title': 'CBWebReader - Add User',
"form": form,
"menu": Menu(request.user, "Users"),
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
"error_message": form.errors,
"success_message": success_message,
"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)
def settings_page(request):
success_message = []
crumbs = [
('Settings', '/comic/settings/'),
]
crumbs = [("Settings", "/comic/settings/")]
if request.POST:
form = SettingsForm(request.POST)
if form.is_valid():
base_dir = Setting.objects.get(name='BASE_DIR')
base_dir.value = form.cleaned_data['base_dir']
base_dir = Setting.objects.get(name="BASE_DIR")
base_dir.value = form.cleaned_data["base_dir"]
base_dir.save()
success_message.append('Settings updated.')
success_message.append("Settings updated.")
form = SettingsForm(initial=SettingsForm.get_initial_values())
context = {
'error_message': form.errors,
'success_message': '</br>'.join(success_message),
'form': form,
'menu': Menu(request.user, 'Settings'),
'title': 'CBWebReader - Settings',
'breadcrumbs': generate_breadcrumbs_from_menu(crumbs),
"error_message": form.errors,
"success_message": "</br>".join(success_message),
"form": form,
"menu": Menu(request.user, "Settings"),
"title": "CBWebReader - Settings",
"breadcrumbs": generate_breadcrumbs_from_menu(crumbs),
}
return render(request, 'comic/settings_page.html', context)
return render(request, "comic/settings_page.html", context)
@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.unread = False
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
else:
status.finished = False
status.save()
title = 'CBWebReader - ' + book.file_name + ' - Page: ' + str(page)
title = "CBWebReader - " + book.file_name + " - Page: " + str(page)
context = {
'book': book,
'orig_file_name': book.page_name(page),
'nav': book.nav(page, request.user),
'breadcrumbs': breadcrumbs,
'menu': Menu(request.user),
'title': title,
"book": book,
"orig_file_name": book.page_name(page),
"nav": book.nav(page, request.user),
"breadcrumbs": breadcrumbs,
"menu": Menu(request.user),
"title": title,
}
return render(request, 'comic/read_comic.html', context)
return render(request, "comic/read_comic.html", context)
@login_required
@@ -344,34 +330,29 @@ def get_image(_, comic_selector, page):
def initial_setup(request):
if User.objects.all().exists():
return redirect('/comic/')
return redirect("/comic/")
if request.POST:
form = InitialSetupForm(request.POST)
if form.is_valid():
user = User(
username=form.cleaned_data['username'],
email=form.cleaned_data['email'],
username=form.cleaned_data["username"],
email=form.cleaned_data["email"],
is_staff=True,
is_superuser=True,
)
user.set_password(form.cleaned_data['password'])
user.set_password(form.cleaned_data["password"])
user.save()
base_dir, _ = Setting.objects.get_or_create(name='BASE_DIR')
base_dir.value = form.cleaned_data['base_dir']
base_dir, _ = Setting.objects.get_or_create(name="BASE_DIR")
base_dir.value = form.cleaned_data["base_dir"]
base_dir.save()
user = authenticate(username=form.cleaned_data['username'],
password=form.cleaned_data['password'])
user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"])
login(request, user)
return redirect('/comic/')
return redirect("/comic/")
else:
form = InitialSetupForm()
context = {
'form': form,
'title': 'CBWebReader - Setup',
'error_message': form.errors,
}
return render(request, 'comic/settings_page.html', context)
context = {"form": form, "title": "CBWebReader - Setup", "error_message": form.errors}
return render(request, "comic/settings_page.html", context)
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):
username = forms.CharField(max_length=50,
label='',
username = forms.CharField(
max_length=50,
label="",
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': 'Username',
'autofocus': True,
'required': True,
}
))
password = forms.CharField(label='Password',
widget=forms.PasswordInput(
attrs={
'class': 'form-control',
'placeholder': 'Username',
'required': True,
}
))
attrs={"class": "form-control", "placeholder": "Username", "autofocus": True, "required": True}
),
)
password = forms.CharField(
label="Password",
widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "Username", "required": True}),
)
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
if settings.CBREADER_USE_RECAPTCHA if hasattr(settings, 'CBREADER_USE_RECAPTCHA') else False:
self.fields['captcha'] = ReCaptchaField(widget=ReCaptchaWidget())
if settings.CBREADER_USE_RECAPTCHA if hasattr(settings, "CBREADER_USE_RECAPTCHA") else False:
self.fields["captcha"] = ReCaptchaField(widget=ReCaptchaWidget())

View File

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

View File

@@ -14,11 +14,11 @@ services:
- database
ports:
- "8000:8000"
volumes:
- ./cbreader:/src/cbreader
- ./comic:/src/comic
- ./comic_auth:/src/comic_auth
- ${COMIC_BOOK_VOLUME}:/data
# volumes:
# - ./cbreader:/src/cbreader
# - ./comic:/src/comic
# - ./comic_auth:/src/comic_auth
# - ${COMIC_BOOK_VOLUME}:/data
command: python manage.py runserver 0.0.0.0:8000
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
setup(
name='cbwebreader',
version='',
packages=['comic', 'comic.migrations', 'cbreader', 'comic_auth', 'comic_auth.migrations'],
url='https://github.com/ajurna/cbwebreader',
license='http://creativecommons.org/licenses/by-sa/4.0/',
author='Ajurna',
author_email='ajurna@gmail.com',
description='Comic Book Web Reader', requires=['django-recaptcha', 'django', 'ujson']
name="cbwebreader",
version="",
packages=["comic", "comic.migrations", "cbreader", "comic_auth", "comic_auth.migrations"],
url="https://github.com/ajurna/cbwebreader",
license="http://creativecommons.org/licenses/by-sa/4.0/",
author="Ajurna",
author_email="ajurna@gmail.com",
description="Comic Book Web Reader",
requires=["django-recaptcha", "django", "ujson"],
)