59 Commits

Author SHA1 Message Date
fd06a6c72e python3 uwsgi plugin 2021-02-15 12:08:23 +01:00
85fa11315b fixed(??) start.sh 2021-02-15 11:58:30 +01:00
12d13635ad fixed(?) start.sh 2021-02-15 11:55:52 +01:00
5fe4e94e73 added uwsgi to install 2021-02-15 11:53:34 +01:00
e1c66fd034 removed hardcoded static files 2021-02-15 11:51:18 +01:00
7375ffe830 changed db path 2021-02-15 11:50:20 +01:00
8ba82ef0db changed log folder 2021-02-15 11:49:33 +01:00
f7093e5e58 added calibre dir 2021-02-15 11:48:22 +01:00
19c5b0830a remove settings from gitignore 2021-02-15 11:46:49 +01:00
23c1ff7140 no more .bak settings 2021-02-15 11:43:39 +01:00
8187817752 even more fixes 2021-02-15 11:42:40 +01:00
9160a37378 more fixes 2021-02-15 11:41:31 +01:00
fd77792688 fixes 2021-02-15 11:39:49 +01:00
9f5e2e93dd slight fixes to file structure in dockerfile 2021-02-15 11:38:04 +01:00
3a2a2ce268 updates 2021-02-15 11:35:11 +01:00
48443d9855 initial commit 2021-02-15 11:13:30 +01:00
d7a385fd45 changed instructions 2020-08-16 02:11:33 -03:00
edc9366a5b changed instructions 2020-08-16 02:06:22 -03:00
88fcb17dc5 dev fixes
Merge branch 'development'
2020-08-16 01:47:38 -03:00
49ca4bccdc fixed nginx, bug in context processors 2020-08-16 01:45:28 -03:00
3985a5635d Merge branch 'development' of https://git.tau.aperturect.com/MassiveAtoms/calibre-web-companion into development 2020-08-15 11:04:49 -03:00
9843299ef6 fixed some db stuff 2020-08-15 11:00:25 -03:00
75099ca05e Removed hardcoded paths, make workflow changes
I removed some hardcoded paths for logging and where the default db should be located.
I also organized deployment stuff a bit
2020-08-15 02:40:03 -03:00
1b8d81bd3c pre fixing problems 2020-08-15 01:56:27 -03:00
75e78d606f added tests, fixed search bug, and 'global context variable' bug 2020-08-04 12:21:17 -03:00
f9478f2894 merge dev to master 2020-08-02 11:56:32 -03:00
b235f67be3 some readme stuff 2020-08-02 11:54:47 -03:00
43e5d71cec logging, deployment stuff and readme 2020-08-02 11:21:43 -03:00
e11ae55ed9 deployment, and some load testing 2020-08-01 23:51:34 -03:00
5182b2cdb6 tested deployment 2020-08-01 22:47:12 -03:00
6a2f89d36e fixed book download, linux path is case sensitive 2020-08-01 20:07:09 -03:00
ed03ea4a1c stuff 2020-08-01 19:54:04 -03:00
0806b55cbe clarification 2020-07-31 17:24:55 -03:00
10648a6d0a Revert "clarification readme"
This reverts commit e62e54757a.
2020-07-31 17:07:30 -03:00
e62e54757a clarification readme 2020-07-31 17:05:23 -03:00
af1bfc06a5 easyColab, removeSpice 2020-07-17 01:39:00 -03:00
d56911901b added benchmarking 2020-07-17 00:48:54 -03:00
017e473b4d changed db routing to better facilitate plugin support 2020-07-16 17:28:59 -03:00
b65ef99935 roadmap 2020-07-16 14:33:57 -03:00
aa3151dde6 roadmap 2020-07-16 14:30:45 -03:00
53273fa3c2 removed pyc 2020-07-16 13:48:50 -03:00
2887fd852a colab stuff 2020-07-16 13:12:47 -03:00
acb2ab9e52 remove gitignored 2020-07-16 13:10:11 -03:00
eca48c6cfa what about now 2020-07-16 13:08:08 -03:00
4e18118605 Merge pull request 'optimize' (#2) from optimize into master
Reviewed-on: MassiveAtoms/Calibre-Server#2
2020-07-16 16:01:58 +00:00
7a46de1679 better colab 2020-07-16 12:53:58 -03:00
398088454b did stuff 2020-07-16 12:50:38 -03:00
9f0d46a17a stuff 2020-07-16 12:49:36 -03:00
071b82121c Merge pull request 'optimize' (#1) from optimize into master
Reviewed-on: MassiveAtoms/Calibre-Server#1
2020-07-16 15:47:00 +00:00
ac1d7fb5e8 removed files 2020-07-16 12:45:35 -03:00
915d8369bf better colab settings 2020-07-16 12:43:58 -03:00
1efa9b2166 added cache 2020-07-15 20:30:04 -03:00
777e949c9f fixed publisherdetail 2020-07-15 13:29:32 -03:00
32a2b3e6c1 css concepts 2020-07-15 13:26:13 -03:00
14f14a2f33 ??? 2020-07-15 12:20:35 -03:00
d57f677bd3 add ugly search form in base template 2020-07-15 10:42:49 -03:00
dc1571bcfb added identifier option to search 2020-07-15 00:34:44 -03:00
5a83ccfc07 fixed some templating issues 2020-07-14 19:48:54 -03:00
f13132d0c7 fixed requirements 2020-07-14 19:19:27 -03:00
64 changed files with 1553 additions and 221 deletions

149
.gitignore vendored Normal file
View File

@ -0,0 +1,149 @@
# project specific
#settings.json
db.sqlite3
dummyusers.json
*.prof
# IDE
.vscode
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.pyc
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
watering-env/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

View File

@ -11,43 +11,99 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
import json
import logging
logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
with open(BASE_DIR + "/settings.json", "r") as userfile:
usersettings = json.load(userfile)
CALIBRE_DIR = os.path.abspath(usersettings["CALIBRE_DIR"])
SECRET_KEY = usersettings["SECRET_KEY"]
ALLOWED_HOSTS = usersettings["ALLOWED_HOSTS"]
INTERNAL_IPS = usersettings["INTERNAL_IPS"]
DEBUG = usersettings["DEBUG"]
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CALIBRE_DIR = os.path.abspath(
"C:\\Users\\MassiveAtoms\\Documents\\Calibre Library")
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# optimisation stuff ###############################################3
# #
CONN_MAX_AGE = 60 * 5
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
"TIMEOUT": 60 * 5,
}
}
## ##
########################################################################
## STATIC FILES ##
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATICFILES_DIRS = [
os.path.abspath(CALIBRE_DIR),
# os.path.abspath(CALIBRE_DIR),
# '/static/',
]
STATIC_URL = '/static/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'u(8^+rb%rz5hsx4v^^y(ul7g(4n7a8!db@s*9(m5cs*2_ppy8+'
STATIC_ROOT = BASE_DIR + "/static/"
## ##
#########################################################################
# LOGGING
ALLOWED_HOSTS = ['127.0.0.1', ]
INTERNAL_IPS = [
# ...
'127.0.0.1',
# ...
]
# Don't change things beyond this
logfile = usersettings["LOGFOLDER"] + "django.log"
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"root": {"level": "INFO", "handlers": ["file"]},
"handlers": {
"file": {
"level": "INFO",
"class": "logging.FileHandler",
"filename": logfile,
"formatter": "app",
},
},
"loggers": {
"django": {
"handlers": ["file"],
"level": "INFO",
"propagate": True
},
},
"formatters": {
"app": {
"format": (
u"%(asctime)s [%(levelname)-8s] "
"(%(module)s.%(funcName)s) %(message)s"
),
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
}
## ##
########################################################################
## DERUG ##
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.timer.TimerPanel',
@ -62,9 +118,15 @@ DEBUG_TOOLBAR_PANELS = [
]
## ##
########################################################################
## DERUG ##
# SILKY_PYTHON_PROFILER = True
# SILKY_PYTHON_PROFILER_BINARY = True
# SILKY_PYTHON_PROFILER_RESULT_PATH = BASE_DIR + "/profiler"
# SILKY_META = True
LOGIN_REDIRECT_URL = '/books'
@ -78,11 +140,14 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
"library",
'debug_toolbar', # DEBUG purposes
# "silk", # DEBUG/profilling purposes
# 'debug_toolbar', # DEBUG purposes
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware', # DEBUG purposes
# 'silk.middleware.SilkyMiddleware', # DEBUG/profiling purposes
# 'debug_toolbar.middleware.DebugToolbarMiddleware', # DEBUG purposes
'django.middleware.cache.UpdateCacheMiddleware', # cache
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@ -90,7 +155,11 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware', # cache
]
## ##
########################################################################
DEFAULT_CHARSET = "utf-8"
ROOT_URLCONF = 'CalibreWebCompanion.urls'
@ -115,14 +184,20 @@ TEMPLATES = [
WSGI_APPLICATION = 'CalibreWebCompanion.wsgi.application'
# Database
## ##
########################################################################
## DATBASE ##
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
if usersettings["ISDOCKER"]:
defaultdb_path = "calibre"
else:
defaultdb_path = BASE_DIR
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'NAME': os.path.join(defaultdb_path, 'db.sqlite3'),
},
'calibre': {
'ENGINE': 'django.db.backends.sqlite3',
@ -131,7 +206,7 @@ DATABASES = {
}
DATABASE_ROUTERS = ["db_routers.DjangoRouter", "db_routers.CalibreRouter"]
DATABASE_ROUTERS = ["db_routers.CalibreRouter", "db_routers.DjangoRouter"]
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators

View File

@ -22,6 +22,7 @@ from django.conf import settings
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),
@ -30,8 +31,9 @@ urlpatterns = [
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG: # DEBUG purposes
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
# if settings.DEBUG: # DEBUG purposes
# urlpatterns+= [path('silk/', include('silk.urls', namespace='silk'))]
# import debug_toolbar
# urlpatterns = [
# path('__debug__/', include(debug_toolbar.urls)),
# ] + urlpatterns

View File

@ -0,0 +1,21 @@
[uwsgi]
base = /cwebcomp
chdir = %(base)
home = %(base)
pidfile= %(base)/cwebcomp.pid
pythonpath= /usr/local
uid = www-data
gid = www-data
module = wsgi:application # path to wsgy.py file
socket = :8000
processes = 8
threads = 4
master = true
chmod-socket = 660
vacuum = true
die-on-term = true
harakiri = 20
max-requests = 5000
logs = %(base)/uwsgi_info.logs
daemonize = %(base)/uwsgi.logs
plugins = python3

View File

@ -1,26 +1,51 @@
import logging
logger = logging.getLogger(__name__)
class DjangoRouter:
"""
A router to control all database operations on models in the
auth and contenttypes applications.
"""
route_app_labels = {'auth', 'contenttypes', "sessions", "sites", "admin", "flatpages"}
def db_for_read(self, model, **hints):
"""
Attempts to read anything else goes to calibre
"""
return 'default'
def db_for_write(self, model, **hints):
"""
Attempts to write auth and contenttypes models go to 'calibre'.
"""
return 'default'
def allow_relation(self, obj1, obj2, **hints):
"""
Allow relations.
"""
return True
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
Yes
"""
return True
class CalibreRouter:
"""
A router to control all database operations on models in the
auth and contenttypes applications.
"""
route_app_labels = {"library"}
def db_for_read(self, model, **hints):
"""
Attempts to read auth and contenttypes models go to default.
"""
if model._meta.app_label in self.route_app_labels:
return 'default'
return None
def db_for_write(self, model, **hints):
"""
Attempts to write auth and contenttypes models go to django.
"""
if model._meta.app_label in self.route_app_labels:
return 'default'
return 'calibre'
return None
def allow_relation(self, obj1, obj2, **hints):
@ -35,42 +60,19 @@ class DjangoRouter:
return True
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""
Make sure the auth and contenttypes apps only appear in the
'django' database.
"""
if app_label in self.route_app_labels:
return db == 'default'
return None
class CalibreRouter:
"""
A router to control all database operations on models in the
auth and contenttypes applications.
"""
def db_for_read(self, model, **hints):
"""
Attempts to read anything else goes to calibre
"""
return 'calibre'
# def db_for_write(self, model, **hints): # might be prudent not to allow writes
# def allow_migrate(self, db, app_label, model_name=None, **hints):
# """
# Attempts to write auth and contenttypes models go to 'calibre'.
# Make sure the auth and contenttypes apps only appear in the
# 'django' database.
# """
# return 'calibre'
# if app_label in self.route_app_labels:
# return db == 'default'
# return None
def allow_relation(self, obj1, obj2, **hints):
"""
Allow relations.
"""
return True
# def allow_migrate(self, db, app_label, model_name=None, **hints): # might be prudent not to allow migrations
# def db_for_write(self, model, **hints):
# """
# Yes
# Attempts to write auth and contenttypes models go to django.
# """
# return True
# if model._meta.app_label in self.route_app_labels:
# return 'default'
# return None

View File

@ -0,0 +1,33 @@
import multiprocessing
import os
import json
bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
preload_app = True # By preloading an application you can save some RAM resources as well as speed up server boot times
keepalive = 5
# daemon = True # Detaches the server from the controlling terminal and enters the background. disabled for now
# logging
with open("settings.json", "r") as jfile:
settings = json.load(jfile)
errorlog = settings["LOGFOLDER"] + "/gunicorn_error.log"
loglevel = "warning"
accesslog = settings["LOGFOLDER"] + "/gunicorn_access.log"
if not os.path.isdir("/usr/src/app/data/logs"):
os.mkdir("/usr/src/app/data/logs")
if not os.path.isfile(errorlog):
os.system('touch {}'.format(errorlog))
if not os.path.isfile(accesslog):
os.system('touch {}'.format(accesslog))
capture_output = True
# debug settings which need to be commented out in prod
# reload=True
# reload_engine = "inotify"
# I only went till the section https://docs.gunicorn.org/en/latest/settings.html#logging there are more settings
# some of them might be useful

View File

@ -1,6 +1,8 @@
from .models import Author, Tag, Publisher, Language, Rating, Series
from django.db.models import Count
import logging
logger = logging.getLogger(__name__)
def filters(request):
# unique_authors = Author.objects.all().order_by('sort')
@ -13,7 +15,7 @@ def filters(request):
unique_authors = Author.objects.only('name', "id").annotate(num_books=Count('book')).order_by('name')
unique_tags = Tag.objects.annotate(num_books=Count('book')).order_by('name')
unique_publishers = Publisher.objects.annotate(num_books=Count('book')).order_by('name')
unique_languages = Language.objects.annotate(num_books=Count('book')).order_by('rating')
unique_languages = Language.objects.annotate(num_books=Count('book')).order_by('lang_code')
unique_ratings = Rating.objects.annotate(num_books=Count('book'))
unique_series = Series.objects.annotate(num_books=Count('book')).order_by('sort')

View File

@ -1,12 +1,15 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
import logging
logger = logging.getLogger(__name__)
class SearchForm(forms.Form):
title = forms.CharField(label="Title", max_length=200)
author = forms.CharField(label='Author', max_length=100)
# identifier = forms.CharField(label='Identifier(ISBN, Google-id, amazon id)', max_length=20)
identifier = forms.CharField(label='Identifier(ISBN, Google-id, amazon id)', max_length=20)
generic = forms.CharField(label='All', max_length=100, required=False)

View File

@ -7,7 +7,10 @@
# Feel free to rename the models, but don't rename db_table values or field names.
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
import logging
logger = logging.getLogger(__name__)
class Author(models.Model):
name = models.TextField()
@ -56,7 +59,7 @@ class Data(models.Model):
class Identifier(models.Model):
book = models.IntegerField()
book = models.ForeignKey("Book", db_column="book", on_delete=models.CASCADE)
type = models.TextField()
val = models.TextField()
@ -173,9 +176,9 @@ class Book(models.Model):
title = models.TextField()
sort = models.TextField(blank=True, null=True)
# This field type is a guess.
timestamp = models.TextField(blank=True, null=True)
timestamp = models.DateTimeField(blank=True, null=True)
# This field type is a guess.
pubdate = models.TextField(blank=True, null=True)
pubdate = models.DateTimeField(blank=True, null=True)
series_index = models.FloatField()
author_sort = models.TextField(blank=True, null=True)
isbn = models.TextField(blank=True, null=True)
@ -184,7 +187,7 @@ class Book(models.Model):
flags = models.IntegerField()
uuid = models.TextField(blank=True, null=True)
has_cover = models.BooleanField(blank=True, null=True)
last_modified = models.TextField() # This field type is a guess.
last_modified = models.DateTimeField() # This field type is a guess.
authors = models.ManyToManyField(
Author,
through='BookAuthorLink',
@ -224,9 +227,9 @@ class Book(models.Model):
through='BookRatingLink',
through_fields=('book', 'rating'))
@property
@cached_property
def rating(self):
return self.rating.first()
return self.ratings.first()
def get_absolute_url(self):
"""Returns the url to access a particular instance of MyModelName."""
@ -382,25 +385,3 @@ class BookTagLink(models.Model):
# class Meta:
# managed = False
# db_table = 'feeds'
#
#
# class LastReadPositions(models.Model):
# book = models.IntegerField()
# format = models.TextField()
# user = models.TextField()
# device = models.TextField()
# cfi = models.TextField()
# epoch = models.FloatField()
# pos_frac = models.FloatField()
#
# class Meta:
# managed = False
# db_table = 'last_read_positions'
# class MetadataDirtied(models.Model):
# book = models.IntegerField()
# class Meta:
# managed = False
# db_table = 'metadata_dirtied'

View File

@ -3,10 +3,8 @@
<head>
{% block title %}<title>Local Library</title>{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Compiled and minified CSS -->
@ -14,6 +12,16 @@
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
<style>
p.count {
color: #FFFFFF;
background-color: #515151;
border: 1px #303030;
border-radius: 0.5rem;
padding: .2rem .25rem;
margin: 0.1rem 0.1rem .1rem;
}
table {
width: 100%;
table-layout: fixed;
@ -24,7 +32,7 @@
}
.title {
width: 40%;
width: 30%;
}
.author {
@ -36,17 +44,23 @@
}
.tags {
width: 15%;
width: 25%;
}
.added {
width: 20%;
width: 10%;
}
.published {
width: 10%;
}
</style>
</head>
<body>
<div class="navbar-fixed">
<nav>
<div class="nav-wrapper row green darken-1">
@ -56,16 +70,36 @@
<li class="active"><a href="{{user.get_absolute_url}}"> {{ user.get_username }}</a></li>
<li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
</ul>
{% load cache %}
{% cache 500 sidebar request.user.username %}
<!--Maybe i'm retarded but this is not caching versions per user-->
<ul class="left">
<li><a href="{% url 'search' %}">Search</a></li>
<li><a href="{% url 'books' %}">Books</a></li>
<li><a class="dropdown-trigger" href="#!" data-target="dropdown-authors">Authors<i
<li><a class="dropdown-trigger" href={% url 'authors' %} data-target="dropdown-authors">Authors<i
class="material-icons right">arrow_drop_down</i></a></li>
<li><a class="dropdown-trigger" href="#!" data-target="dropdown-ratings">Ratings<i
<li><a class="dropdown-trigger" href={% url "ratings" %} data-target="dropdown-ratings">Ratings<i
class="material-icons right">arrow_drop_down</i></a></li>
<li><a class="dropdown-trigger" href="#!" data-target="dropdown-tags">Tags<i
<li><a class="dropdown-trigger" href={% url "tags" %} data-target="dropdown-tags">Tags<i
class="material-icons right">arrow_drop_down</i></a></li>
<li><a class="dropdown-trigger" href={% url "series" %} data-target="dropdown-series">Series<i
class="material-icons right">arrow_drop_down</i></a></li>
<li><a class="dropdown-trigger" href={% url "publishers" %} data-target="dropdown-pubishers">Publishers<i
class="material-icons right">arrow_drop_down</i></a></li>
<li><a href="{% url 'search' %}">Advanced search</a></li>
<li>
<!-- stefan, this div. can we have this int the navbar? -->
<div class="container">
<form action="{% url 'results' %}" method="get" style="padding-top:2em">
<label for="generic"></label>
<input id="generic" type="text" name="generic" value="">
<button class="waves-effect waves-light btn green accent-4" type="submit">search</button>
</form>
</div>
<!-- this is the end of the div, stefan -->
</li>
</ul>
<ul id="dropdown-authors" class="dropdown-content">
{% for author in unique_authors %}
@ -79,16 +113,29 @@
</ul>
<ul id="dropdown-tags" class="dropdown-content">
{% for tag in unique_tags %}
<li><a href="{{tag.get_absolute_url}}">{{tag}}</a></li>
<li><a href="{{tag.get_absolute_url}}">{{tag}} ({{tag.num_books}})</a></li>
{% endfor %}
</ul>
<ul id="dropdown-series" class="dropdown-content">
{% for tag in unique_series %}
<li><a href="{{tag.get_absolute_url}}">{{tag}} ({{tag.num_books}})</a></li>
{% endfor %}
</ul>
<ul id="dropdown-pubishers" class="dropdown-content">
{% for pub in unique_publishers %}
<!-- stefan here's my shit count -->
<li><a href="{{pub.get_absolute_url}}">{{pub}} <p class="count">{{pub.num_books}}</p> </a> </li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<li><a href="{% url 'sign-up'%}?next={{request.path}}">Sign up</a></li>
<li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
</ul>
{% endif %}
</div>
</nav>
</div>
<script>
@ -99,7 +146,6 @@
});
</script>
{% if user.is_authenticated %}
{% block content %} {% endblock %}
{% else %}
@ -113,7 +159,8 @@
<p>You don't have permission to view this.</p>
</div>
<div class="card-action center">
<a class="waves-effect waves-light btn-large green accent-4" href="{% url 'login'%}?next={{request.path}}">Login</a>
<a class="waves-effect waves-light btn-large green accent-4"
href="{% url 'login'%}?next={{request.path}}">Login</a>
</div>
</div>
</div>

View File

@ -17,7 +17,8 @@
<tr>
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
<td>{{book.author_sort}}</td>
<td> {% for rating in book.ratings.all %}
<td>
{% for rating in book.ratings.all %}
{{rating}}
{% endfor %}
</td>

View File

@ -1,51 +1,52 @@
{% extends "base.html" %}
{% block title %}<title>{{book.title}}</title>{% endblock %}
{% block content %}
{% load static %}
<div class="col s12 m7">
<div class="card z-depth-0 horizontal">
<div class="card-image">
<a style="padding-top:15%" href="{{download}}"><img src=" {% static "" %}{{imgpath}}" alt="download" srcset=""></a>
<a style="padding-top:15%" href=" /download/{{download}}"><img src=" /download/{{imgpath}}"
alt="download" srcset=""></a>
</div>
<div class="card-stacked">
<div class="card-content">
<h1> {{book.title}}</h1>
<h4> by
{% if book.authors %}
{% for author in book.authors.all %}
<a href="{{author.get_absolute_url}}">{{author.name}}</a>
{%endfor%}
{% else %}
{{book.author_sort}}
{%endif%}
<br>
Published by
{% if book.publishers %}
{% for pub in book.publishers.all %}
<a href="{{pub.get_absolute_url}}">{{pub.name}}</a>
{%endfor%}
{% else %}
Unknown
{%endif%}
<br>
Tags:
{% if book.tags %}
{% for tag in book.tags.all %}
<a href="{{tag.get_absolute_url}}">{{tag.name}}</a>,
{%endfor%}
{% else %}
{%endif%}
<br>
Rating:
{% if book.ratings %}
{% for rating in book.ratings.all %}
<a href="{{rating.get_absolute_url}}">{{rating}}</a>
{%endfor%}
{% else %}
{%endif%}
<br>
<a href="{{book.publisher.get_absolute_url}}">{{book.publisher}}</a>
{% if book.authors %}
{% for author in book.authors.all %}
<a href="{{author.get_absolute_url}}">{{author.name}}</a>
{%endfor%}
{% else %}
{{book.author_sort}}
{%endif%}
<br>
Published by
{% if book.publishers %}
{% for pub in book.publishers.all %}
<a href="{{pub.get_absolute_url}}">{{pub.name}}</a>
{%endfor%}
{% else %}
Unknown
{%endif%}
<br>
Tags:
{% if book.tags %}
{% for tag in book.tags.all %}
<a href="{{tag.get_absolute_url}}">{{tag.name}}</a>,
{%endfor%}
{% else %}
{%endif%}
<br>
Rating:
{% if book.ratings %}
{% for rating in book.ratings.all %}
<a href="{{rating.get_absolute_url}}">{{rating}}</a>
{%endfor%}
{% else %}
{%endif%}
<br>
<a href="{{book.publisher.get_absolute_url}}">{{book.publisher}}</a>
</h4>
</div>
</div>
@ -53,9 +54,9 @@
</div>
<div class="container">
{% autoescape off %}
{{comment}}
{% endautoescape %}
{% autoescape off %}
{{comment}}
{% endautoescape %}
</div>
{% endblock %}

View File

@ -2,40 +2,67 @@
{% block content %}
<style>
/* stefan, this is my tag style */
.tags a {
color: #FFFFFF;
background-color: #43A047;
text-transform: uppercase;
font-size: .66rem;
white-space: nowrap;
border-radius: 2rem;
padding: .25rem .85rem .25rem;
line-height: 2;
margin: 0.1rem 0.1rem .1rem;
}
.tags {
width: 25vw;
padding: .5rem 0 1rem;
line-height: 2;
display: flex;
flex-flow: row wrap;
}
</style>
<h1 class="center">Book List</h1>
<div class="row">
<div class="col s1 m0">
</div>
<div class="col s10 m12">
<table id="books" class="highlight centered">
<tr>
<!--When a header is clicked, run the sortTable function, with a parameter, 0 for sorting by names, 1 for sorting by country:-->
<th class="title" onclick="sortTable(0)">Title</th>
<th class="author" onclick="sortTable(1)">Author</th>
<th class="rating" onclick="sortTable(2)">Rating</th>
<th class="tags" onclick="sortTable(3)">Tags</th>
<th class="added" onclick="sortTable(4)">Added</th>
</tr>
{% for book in book_list %}
<tr>
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
<td>{{book.author_sort}}</td>
<td> {% for rating in book.ratings.all %}
{{rating}}
<div class="col s1 m0">
</div>
<div class="col s10 m12">
<table id="books" class="highlight centered">
<tr>
<!--When a header is clicked, run the sortTable function, with a parameter, 0 for sorting by names, 1 for sorting by country:-->
<th class="title" onclick="sortTable(0)">Title</th>
<th class="author" onclick="sortTable(1)">Author</th>
<th class="rating" onclick="sortTable(2)">Rating</th>
<th class="tags" onclick="sortTable(3)">Tags</th>
<th class="added" onclick="sortTable(4)">Added</th>
<th class="published" onclick="sortTable(5)">Published</th>
</tr>
{% for book in book_list %}
<tr>
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
<td>{{book.author_sort}}</td>
<td> {% for rating in book.ratings.all %}
{{rating}}
{% endfor %}
</td>
<td class="tags">
<!-- stefan -->
{% for tag in book.tags.all %}
<a href={{tag.get_absolute_url}} rel="tag">{{tag}}</a>
{% endfor %}
</td>
<td>{{book.timestamp | date:"d/m/Y" }}</td>
<td>{{book.pubdate.year}}</td>
</tr>
{% endfor %}
</td>
<td>
{% for tag in book.tags.all %}
{{tag}},
{% endfor %}
</td>
<td>{{book.timestamp}}</td>
</tr>
{% endfor %}
</table>
<div class="col s1 m0">
</div>
</div>
</table>
<div class="col s1 m0">
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -18,7 +18,7 @@
<tr>
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
<td>{{book.author_sort}}</td>
<td> {% for rating in book.rating.all %}
<td> {% for rating in book.ratings.all %}
{{rating}}
{% endfor %}
</td>

View File

@ -17,9 +17,8 @@
<tr>
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
<td>{{book.author_sort}}</td>
<td> {% for rating in book.rating.all %}
<td>
{{rating}}
{% endfor %}
</td>
<td>
{% for tag in book.tags.all %}

View File

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block content %}
<h1>{{Series}} </h1>
<table id="books" class="highlight centered">
<tr>
<!--When a header is clicked, run the sortTable function, with a parameter, 0 for sorting by names, 1 for sorting by country:-->
<th class="title" onclick="sortTable(0)">Title</th>
<th class="author" onclick="sortTable(1)">Author</th>
<th class="rating" onclick="sortTable(2)">Rating</th>
<th class="tags" onclick="sortTable(3)">Tags</th>
<th class="added" onclick="sortTable(4)">Added</th>
</tr>
{% for book in books %}
<tr>
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
<td>{{book.author_sort}}</td>
<td> {% for rating in book.ratings.all %}
{{rating}}
{% endfor %}
</td>
<td>
{% for tag in book.tags.all %}
{{tag}},
{% endfor %}
</td>
<td>{{book.timestamp}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<h1>Author List</h1>
{% if series_list %}
<ul>
{% for series in series_list %}
<li>
<a href="{{ series.get_absolute_url }}">{{ series.name }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>There are no series in the library.</p>
{% endif %}
{% endblock %}

View File

@ -22,7 +22,7 @@
{% endfor %}
</td>
<td>
{% for tag in book.tag.all %}
{% for tag in book.tags.all %}
{{tag}},
{% endfor %}
</td>

View File

@ -8,6 +8,8 @@
<input id="title" type="text" name="title" value="">
<label for="author">Author: </label>
<input id="author" type="text" name="author" value="">
<label for="author">Identifier: </label>
<input id="identifier" type="text" name="identifier" value="">
<button class="waves-effect waves-light btn green accent-4" type="submit">search</button>
</form>
</div>

View File

@ -1,3 +1,166 @@
from django.test import TestCase
from django.test import Client, TestCase
from pprint import pprint
from django.test.utils import setup_test_environment
from .models import Book, Author, Publisher, Series, Rating, Tag, Identifier
from django.db.models import Count
# Create your tests here.
client = Client()
client.login(username="testuser", password="dumbeasypassword")
def booklisttest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/books/")
assert res.status_code == 200
context = dict(res.context)
assert sorted(context["book_list"], key=lambda x: x.id)== sorted(Book.objects.all(), key=lambda x: x.id)
def authorlisttest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/authors/")
assert res.status_code == 200
context = dict(res.context)
assert sorted(context["author_list"], key=lambda x: x.id)== sorted(Author.objects.all(), key=lambda x: x.id)
def publisherlisttest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/publishers/")
assert res.status_code == 200
context = dict(res.context)
assert sorted(context["publisher_list"], key=lambda x: x.id)== sorted(Publisher.objects.all(), key=lambda x: x.id)
def serieslisttest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/series/")
assert res.status_code == 200
context = dict(res.context)
assert sorted(context["series_list"], key=lambda x: x.id)== sorted(Series.objects.all(), key=lambda x: x.id)
def ratinglisttest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/ratings/")
assert res.status_code == 200
context = dict(res.context)
assert sorted(context["rating_list"], key=lambda x: x.id)== sorted(Rating.objects.all(), key=lambda x: x.id)
def taglisttest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/tags/")
assert res.status_code == 200
context = dict(res.context)
assert sorted(context["tag_list"], key=lambda x: x.id)== sorted(Tag.objects.all(), key=lambda x: x.id)
def bookdetailtest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
ids = [i.id for i in Book.objects.all()][:10]
for i in ids:
res = c.get(f"/book/{i}")
assert res.status_code == 200
context = dict(res.context)
assert context["book"] == Book.objects.get(id=i)
def authordetailtest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
ids = [i.id for i in Author.objects.all()][:10]
for i in ids:
res = c.get(f"/author/{i}")
assert res.status_code == 200
context = dict(res.context)
assert context["author"] == Author.objects.get(id=i)
def publisherdetailtest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
ids = [i.id for i in Publisher.objects.all()][:10]
for i in ids:
res = c.get(f"/publisher/{i}")
assert res.status_code == 200
context = dict(res.context)
assert context["publisher"] == Publisher.objects.get(id=i)
def seriesdetailtest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
ids = [i.id for i in Series.objects.all()][:10]
for i in ids:
res = c.get(f"/series/{i}")
assert res.status_code == 200
context = dict(res.context)
assert context["series"] == Series.objects.get(id=i)
def ratingdetailtest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
ids = [i.id for i in Rating.objects.all()][:10]
for i in ids:
res = c.get(f"/rating/{i}")
assert res.status_code == 200
context = dict(res.context)
assert context["rating"] == Rating.objects.get(id=i)
def tagdetailtest():
c = Client()
c.login(username="testuser", password="dumbeasypassword")
ids = [i.id for i in Tag.objects.all()][:10]
for i in ids:
res = c.get(f"/tag/{i}")
assert res.status_code == 200
context = dict(res.context)
assert context["tag"] == Tag.objects.get(id=i)
def search_partial(key, value, book=None):
c = Client()
c.login(username="testuser", password="dumbeasypassword")
res = c.get("/results/", {key : value})
if not book:
return dict(res.context)["book_list"]
return book in dict(res.context)["book_list"]
def searchtest():
books = [i for i in Book.objects.all()][:10]
for i in books:
assert search_partial("title", i.title, i)
assert search_partial("generic", i.title, i)
assert search_partial("author", i.author_sort, i)
author = i.authors.first()
if author:
assert search_partial("author", author.name, i)
assert search_partial("generic", author.name, i)
assert search_partial("generic", i.author_sort, i)
id = Identifier.objects.filter(book=i.id).first()
if id:
assert search_partial("identifier", id, i)
assert search_partial("generic", id, i)
booklisttest()
bookdetailtest()
authorlisttest()
authordetailtest()
publisherdetailtest()
publisherlisttest()
seriesdetailtest()
serieslisttest()
ratingdetailtest()
ratinglisttest()
tagdetailtest()
taglisttest()
searchtest()

View File

@ -1,7 +1,9 @@
from django.urls import path
from . import views
from django.views.decorators.cache import cache_page
import logging
logger = logging.getLogger(__name__)
urlpatterns = [
path('authors/', views.AuthorListView.as_view(), name='authors'),
@ -9,17 +11,21 @@ urlpatterns = [
path('publishers/', views.PublisherListView.as_view(), name='publishers'),
path('ratings/', views.RatingListView.as_view(), name='ratings'),
path('tags/', views.TagListView.as_view(), name='tags'),
path('author/<int:pk>', views.AuthorDetailView.as_view(), name='author-detail-view'),
path('series/', views.SeriesListView.as_view(), name='series'),
path('author/<int:pk>', views.AuthorDetailView.as_view(),
name='author-detail-view'),
path('book/<int:pk>', views.BookDetailView.as_view(), name='book-detail-view'),
path('publisher/<int:pk>', views.PublisherDetailView.as_view(), name='publisher-detail-view'),
path('rating/<int:pk>', views.RatingDetailView.as_view(), name='rating-detail-view'),
path('publisher/<int:pk>', views.PublisherDetailView.as_view(),
name='publisher-detail-view'),
path('rating/<int:pk>', views.RatingDetailView.as_view(),
name='rating-detail-view'),
path('series/<int:pk>', views.SeriesDetailView.as_view(),
name='series-detail-view'),
path('tag/<int:pk>', views.TagDetailView.as_view(), name='tag-detail-view'),
path('results/', views.ResultsView.as_view(), name='results'),
path('search/', views.SearchView.as_view(), name='search'),
path('accounts/sign_up/',views.sign_up,name="sign-up")
path('accounts/sign_up/', views.sign_up, name="sign-up")
]

View File

@ -1,6 +1,7 @@
from django.utils.decorators import method_decorator
from django.shortcuts import render
from django.views import generic
from .models import Author, Book, Comment, Rating, BookAuthorLink, Publisher, Tag, BookTagLink, BookRatingLink, Data
from .models import Author, Book, Comment, Rating, BookAuthorLink, Publisher, Tag, BookTagLink, BookRatingLink, Data, Identifier, Series
from django.http import HttpResponseRedirect
from .forms import SearchForm, UserCreationForm
from django.db import models
@ -8,6 +9,11 @@ from django.db.models import Q
from django.contrib.auth.models import User
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
import logging
logger = logging.getLogger(__name__)
# might be helpful for vary headers later
@login_required
@ -30,6 +36,9 @@ def sign_up(request):
class SearchView(generic.TemplateView):
template_name = 'search.html'
def dispatch(self, *args, **kwargs):
return super(SearchView, self).dispatch(*args, **kwargs)
class ResultsView(generic.ListView): # no clue if this is secure.
# according to this https://stackoverflow.com/questions/13574043/how-do-django-forms-sanitize-text-input-to-prevent-sql-injection-xss-etc
@ -37,23 +46,59 @@ class ResultsView(generic.ListView): # no clue if this is secure.
model = Book
template_name = 'results.html'
def dispatch(self, *args, **kwargs):
return super(ResultsView, self).dispatch(*args, **kwargs)
def get_queryset(self): # new
title = self.request.GET.get('title')
author = self.request.GET.get('author')
identifier = self.request.GET.get("identifier")
generic = self.request.GET.get("generic")
books = Book.objects.prefetch_related("tags", "ratings")
if title:
books =books.filter(sort__icontains=title)
books = books.filter(sort__icontains=title)
if author:
books = books.filter(author_sort__icontains=author)
# authors are stored as author_sort and author, needs to be slightly more complex
author_obj = Author.objects.filter(name__icontains=author).first()
if not author_obj:
author_id = -1
else:
author_id = author_obj.id
books = books.filter(
Q(author_sort__icontains=author) |
Q(authors__id=author_id)
)
if identifier:
books = books.filter(identifier__val=identifier)
if generic:
author_obj = Author.objects.filter(name__icontains=generic).first()
if not author_obj:
author_id = -1
else:
author_id = author_obj.id
books = books.filter(
Q(sort__icontains=generic) |
Q(author_sort__icontains=generic) |
Q(authors__id=author_id) |
Q(identifier__val=generic)
)
return books
class AuthorListView(generic.ListView):
model = Author
def dispatch(self, *args, **kwargs):
return super(AuthorListView, self).dispatch(*args, **kwargs)
class BookListView(generic.ListView):
model = Book
def dispatch(self, *args, **kwargs):
return super(BookListView, self).dispatch(*args, **kwargs)
def get_queryset(self):
# Annotate the books with ratings, tags, etc
# books = Book.objects.annotate(
@ -64,18 +109,37 @@ class BookListView(generic.ListView):
class PublisherListView(generic.ListView):
model = Publisher
def dispatch(self, *args, **kwargs):
return super(PublisherListView, self).dispatch(*args, **kwargs)
class RatingListView(generic.ListView):
model = Rating
def dispatch(self, *args, **kwargs):
return super(RatingListView, self).dispatch(*args, **kwargs)
class SeriesListView(generic.ListView): # make url entry and template, sometime
model = Series
def dispatch(self, *args, **kwargs):
return super(SeriesListView, self).dispatch(*args, **kwargs)
class TagListView(generic.ListView):
model = Tag
def dispatch(self, *args, **kwargs):
return super(TagListView, self).dispatch(*args, **kwargs)
class AuthorDetailView(generic.DetailView):
model = Author
def dispatch(self, *args, **kwargs):
return super(AuthorDetailView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
# Call the base implementation first to get the context
context = super(AuthorDetailView, self).get_context_data(**kwargs)
@ -89,6 +153,9 @@ class AuthorDetailView(generic.DetailView):
class BookDetailView(generic.DetailView):
model = Book
def dispatch(self, *args, **kwargs):
return super(BookDetailView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
# Call the base implementation first to get the context
context = super(BookDetailView, self).get_context_data(**kwargs)
@ -100,13 +167,16 @@ class BookDetailView(generic.DetailView):
pass
context["imgpath"] = context["object"].path + "/cover.jpg"
download = Data.objects.get(book=context["object"].id)
context["download"] = f"{context['object'].path}/{download.name}.{download.format}"
context["download"] = f"{context['object'].path}/{download.name}.{download.format.lower()}"
return context
class PublisherDetailView(generic.DetailView):
model = Publisher
def dispatch(self, *args, **kwargs):
return super(PublisherDetailView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
# Call the base implementation first to get the context
context = super(PublisherDetailView, self).get_context_data(**kwargs)
@ -120,6 +190,9 @@ class PublisherDetailView(generic.DetailView):
class RatingDetailView(generic.DetailView):
model = Rating
def dispatch(self, *args, **kwargs):
return super(RatingDetailView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
# Call the base implementation first to get the context
context = super(RatingDetailView, self).get_context_data(**kwargs)
@ -133,6 +206,9 @@ class RatingDetailView(generic.DetailView):
class TagDetailView(generic.DetailView):
model = Tag
def dispatch(self, *args, **kwargs):
return super(TagDetailView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
# Call the base implementation first to get the context
context = super(TagDetailView, self).get_context_data(**kwargs)
@ -141,3 +217,19 @@ class TagDetailView(generic.DetailView):
books = books.filter(tags=context["object"].id)
context['books'] = sorted(books, key=lambda x: x.title)
return context
class SeriesDetailView(generic.DetailView):
model = Series
def dispatch(self, *args, **kwargs):
return super(SeriesDetailView, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
# Call the base implementation first to get the context
context = super(SeriesDetailView, self).get_context_data(**kwargs)
# Create any data and add it to the context
books = Book.objects.prefetch_related("tags", "ratings")
books = books.filter(series=context["object"].id)
context['books'] = sorted(books, key=lambda x: x.title)
return context

0
CalibreWebCompanion/manage.py Normal file → Executable file
View File

View File

@ -0,0 +1,15 @@
{
"CALIBRE_DIR": "calibre",
"SECRET_KEY": "u(8^+rb%rz5hsx4v^^y(ul7g(4n7a8!db@s*9(m5cs*2_ppy8+",
"ALLOWED_HOSTS": [
"127.0.0.1"
],
"INTERNAL_IPS": [
"127.0.0.1"
],
"DEBUG" : true,
"LOGFOLDER" : "/cwebcomp/logs",
"ISDOCKER" : true
}

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
FROM python:3.9.1-slim-buster
RUN apt-get clean && \
apt-get update && \
apt-get install -y nginx smbclient default-libmysqlclient-dev \
gcc python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 libffi-dev shared-mime-info uwsgi-core uwsgi-plugin-python3
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN mkdir /cwebcomp
WORKDIR /cwebcomp
ADD . /cwebcomp/
# only add this next one if you have static files
RUN mkdir static
RUN pip install -r requirements.txt
RUN python CalibreWebCompanion/manage.py collectstatic
# only if you need celery
#RUN useradd -ms /bin/bash celery
#COPY broker/init.d_celeryd /etc/init.d/celeryd
#COPY broker/celeryd /etc/default/celeryd
# nginx config and script to be run
COPY deployment/docker/nginx.conf /etc/nginx/sites-available/default
COPY deployment/docker/start.sh /usr/local/bin/start.sh
# set proper file permissions
RUN chmod u+x /usr/local/bin/start.sh
EXPOSE 80
CMD ["/bin/bash", "-c", "start.sh"]

View File

@ -1,22 +1,69 @@
# What is CalibreWebAlternative?
This is a web server to the popular book management application Calibre. We found that the builtin webserver was kinda shit, so we're building our own. (make this friendlier later)
# Features
- navbar with tags, series, authors, etc
- Search by author, identifier, title
- authentication
# Some screenshots
Here's how the various lists look like
![booklist](./screenshots/booklist.png)
Book detail
![bookdetail](./screenshots/bookdetail.png)
navbar
![nav](./screenshots/navbar.png)
Adanced search
![booklist](./screenshots/search.png)
# requirements
Django 3.0
Calibre 4.13 (I have not tested it with anything else atm, will be resolved later)
# how to use:
Edit `./CalibreWebCompanion/CalibreWebCompanion/settings`.
Set CALIBREPATH to the path of your library
`./CalibreWebCompanion`
run `./manage.py runserver`
1. [Docker setup](./deployment/instructions.md#user-content-docker-detup)
2. [Non Docker setup](./deployment/instructions.md#user-content-non-docker-detup)
this is in development mode. don't actually use it or release it like this. The debug info it shows is spicy.
# Features
# Ignore pretty much everything below if you're not working on the project
# Profiling
To do profiling, you have to create some dummy users
Unbakify a file `./loadtesting/dummyusers.json.bak` and fill in the credentials for the dummy users
While django is running, open another shell and cd to `./loadtesting` and run `./bench.py`
To have a more interactive session,
comment out
```
run-time = 2m
headless = true
```
in `locust.conf`, and then run `./bench.py`
You can then go to [http://localhost:8089/](http://localhost:8089/) to see live graphs, tweak the number of users and more.
# Finished Features
- [x] Books
- [x] navbar with tags, series, authors, etc
- [x] Search
- [x] authentication
- [x] Cache
- [x] logging
- [x] deploy instructions
# TODO ROADMAP
- [ ] cache with vary headers
- [ ] localisation
- [ ] Beautifying template (only works well on 720p, no other viewports)
- [ ] Setup email functionality (atm, there's only a dummy one, and you can't reset passwords)
- [ ] isolate the styling and templates, so we can swap them out by just swapping directory content
# TODO
- [ ] fix author_detail_view with annotate instead of current implementation

0
Running Normal file
View File

29
deployment/Dockerfile Normal file
View File

@ -0,0 +1,29 @@
## pull official base image
FROM python:slim-buster
EXPOSE 8080
## set work directory
WORKDIR /usr/src/app
## install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
RUN apk add nginx supervisor
# do nginx stuff
RUN adduser -D -g 'www' www
RUN mkdir -p /run/nginx
COPY ./deployment/nginx.conf /etc/nginx/
## copy project
COPY ./CalibreWebCompanion ./CalibreWebCompanion
COPY ./deployment/startupscript.py ./
## gunicorn borks started with supervisord
COPY ./deployment/supervisord.conf /etc/
ENTRYPOINT /usr/bin/supervisord -c /etc/supervisord.conf
# docker run --publish 8000:80 \
# -v '/home/massiveatoms/Desktop/logs:/usr/src/app/data' \
# -v '/run/media/massiveatoms/1AEEEA6EEEEA421D1/Documents and Settings/MassiveAtoms/Documents/Calibre Library/:/usr/src/app/calibredir' \
# --name cw calibreweb:1.0.1

4
deployment/deploy.py Normal file
View File

@ -0,0 +1,4 @@
from os import environ

View File

@ -0,0 +1,20 @@
server {
listen 80;
server_name 127.0.0.1;
charset utf-8;
client_max_body_size 75M;
location /static/ {
alias /cwebcomp/static/;
}
location /media/ {
alias /cwebcomp/media/;
}
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8000;
}
}

View File

@ -0,0 +1,4 @@
#!/bin/bash
uwsgi --ini CalibreWebCompanion/CalibreWebCompanion/uwsgi.ini
nginx -g 'daemon off;'

View File

@ -0,0 +1,45 @@
# Docker setup (no provided docker image atm)
1. clone the repo
2. rename ./calireWebCompanion/settings.json.bak to settings.json
3. change the secret key
4. run `build --tag calibreweb:1.0 . -f ./deployment/Dockerfile` to build the image
5. run your container with your bind/mount your volumes/paths/things
Here's an example of step 5
```
docker run --publish 80:80\
-v '/home/massiveatoms/Desktop/logs:/usr/src/app/data' \
-v '/run/media/massiveatoms/1AEEEA6EEEEA421D/Documents and Settings/MassiveAtoms/Documents/Calibre Library/:/usr/src/app/calibredir' \
--name cw calibreweb:1.0.1
```
your Calibre path/volume/whatever needs to be mounted at `/usr/src/app/calibredir`, and you need to mount a volume for the db and logs at `/usr/src/app/data`
Issues with it at the moment:
1. we still need to do something to create a random secret key. Atm, this would still
# Docker (provided image)
not done yet
# non docker setup
this might need to be modified, since some things have changed to adapt it for docker setup
1. clone repo
2. pip install -r requirements.txt
3. rename the settings.json.bak to settings.json, change logging folder, change secret key, set isdocker to false
4. install gunicorn and nginx
5. move this nginx.conf to /etc/nginx
6. create a user and group `www`
7. make whatever user nginx runs as (for now, www) the owner of calibredir
8. give execute permissions to parent of calibredir
9. cd to repo, run `gunicorn CalibreWebCompanion.wsgi`
10. start nginx `sudo systemctl restart nginx`
11. make steps 9 and 10 happen on startup?
Slight issues with this atm:
1. where to do ssl?
Suggestions:
1. We might want to use sockets instead of ip/port?
2. autostart gunicorn/nginx
3. some extra instrumentation for gunicorn https://docs.gunicorn.org/en/latest/deploy.html

81
deployment/nginx.conf Normal file
View File

@ -0,0 +1,81 @@
worker_processes 1;
# user nobody nogroup;
user www www; # TEMP disabled
# user nobody nobody; # for systems with 'nobody' as a group instead
error_log /usr/src/app/data/logs/nginx.log warn;
# pid /var/run/nginx.pid;
events {
worker_connections 1024; # increase if you have lots of clients
accept_mutex off; # set to 'on' if nginx worker_processes > 1
use epoll; # to enable for Linux 2.6+ MASSIVEATOMS
# 'use kqueue;' to enable for FreeBSD, OSX
}
http {
include mime.types;
# fallback in case we can't determine a type
default_type application/octet-stream;
access_log /var/log/nginx/access.log combined;
sendfile on;
upstream app_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response
# for UNIX domain socket setups
# server unix:/tmp/gunicorn.sock fail_timeout=0;
# for a TCP configuration
server 127.0.0.1:8000 fail_timeout=0;
}
server {
# if no Host match, close the connection to prevent host spoofing
listen 80 default_server;
return 444;
}
server {
listen 80 deferred; # for Linux massiveatoms
# use 'listen 80 accept_filter=httpready;' for FreeBSD
# listen 80;
client_max_body_size 4G;
# set the correct host(s) for your site
server_name localhost 0.0.0.0; # set this to the server url? or ip? we'll see MASSIVEATOMS
keepalive_timeout 5;
# # MASSIVEATOMS
location /download/ {
alias "/usr/src/app/calibredir/";
# Never forget the fact that this little statement being root instead of alias caused us to lose more than a day troubleshooting
}
location /static/ {
alias "/usr/src/app/CalibreWebCompanion/static/";
# Never forget the fact that this little statement being root instead of alias caused us to lose more than a day troubleshooting
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://127.0.0.1:8000;
}
error_page 500 502 503 504 /500.html;
location = /500.html {
root /path/to/app/current/public;
}
}
}

View File

@ -0,0 +1,11 @@
from os import system, chdir
# system("chown -R www:www /usr/src/app/calibredir")
# print("ownership of calibredir changed")
chdir("/usr/src/app/CalibreWebCompanion")
system("python ./manage.py makemigrations")
print("ran makemigrations")
system("python ./manage.py migrate")
print("migrate")

View File

@ -0,0 +1,36 @@
[supervisord]
nodaemon=true
logfile=/tmp/supervisord.log
childlogdir=/tmp
pidfile = /tmp/supervisord.pid
[program:gunicorn]
directory=/usr/src/app/CalibreWebCompanion
command=gunicorn CalibreWebCompanion.wsgi
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=false
startretries=0
startsecs = 0
[program:nginx]
# user=www
command=nginx
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=false
startretries=0
[program:startupscript]
directory=/usr/src/app
command=python ./startupscript.py
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=false
startretries=0

34
loadtesting/bench.py Normal file
View File

@ -0,0 +1,34 @@
import csv
from rich.console import Console
from rich.table import Column, Table
import subprocess
subprocess.run(["locust"])
def floatify(mystring): # floatify probable floats
return f"{float(mystring):3.3f}"
results = dict()
with open("calibre_stats.csv", "r") as cfile:
reader = csv.reader(cfile, delimiter=",")
for row in reader:
if not len(row) or row[0] == "Type":
continue
results[row[0] + " " + row[1]] = {
"median": floatify(row[4]),
"avg": floatify(row[5]),
"min": floatify(row[6]),
"max": floatify(row[7]),
}
console = Console()
table = Table(show_header=True, header_style="bold green")
table.add_column("Action/url")
table.add_column("min")
table.add_column("avg")
table.add_column("median") # destination
table.add_column("max") # source
for k, v in results.items():
table.add_row(k, v["min"], v["avg"], v["median"], v["max"])
console.print(table)

View File

@ -0,0 +1,2 @@
Method,Name,Error,Occurrences
GET,/book/<id>,500 Server Error: Internal Server Error for url: /book/<id>,3
1 Method Name Error Occurrences
2 GET /book/<id> 500 Server Error: Internal Server Error for url: /book/<id> 3

View File

@ -0,0 +1,20 @@
Type,Name,Request Count,Failure Count,Median Response Time,Average Response Time,Min Response Time,Max Response Time,Average Content Size,Requests/s,Failures/s,50%,66%,75%,80%,90%,95%,98%,99%,99.9%,99.99%,99.999%,100%
GET,/accounts/login/,20,0,15,14.807796478271484,8.188486099243164,21.71945571899414,2024.0,0.16739911642581679,0.0,16,17,18,18,20,22,22,22,22,22,22,22
POST,/accounts/login/,20,0,320.0,335.94770431518555,302.3109436035156,504.45032119750977,76378.0,0.16739911642581679,0.0,330,340,340,350,370,500,500,500,500,500,500,500
GET,/author/<id>,40,0,27,27.45213508605957,20.0350284576416,67.98744201660156,30708.725,0.33479823285163357,0.0,27,28,29,29,32,34,68,68,68,68,68,68
GET,/authors/,53,0,33,32.4410987350176,3.3960342407226562,151.21984481811523,39339.0,0.4436076585284145,0.0,33,33,35,36,43,47,73,150,150,150,150,150
GET,/book/<id>,63,3,31,30.65679186866397,20.604372024536133,50.59003829956055,30451.74603174603,0.5273072167413229,0.025109867463872518,31,32,33,33,35,37,37,51,51,51,51,51
GET,/books/,42,0,130.0,110.17770994277228,3.3218860626220703,274.7330665588379,76378.0,0.3515381444942153,0.0,130,130,130,140,160,190,270,270,270,270,270,270
GET,/publisher/<id>,48,0,27,26.85883641242981,17.744064331054688,69.56601142883301,30890.270833333332,0.4017578794219603,0.0,27,27,29,29,31,34,70,70,70,70,70,70
GET,/publishers/,40,0,24,22.84235954284668,4.957437515258789,34.004926681518555,33158.0,0.33479823285163357,0.0,24,26,27,27,28,31,34,34,34,34,34,34
GET,/rating/<id>,53,0,30,36.589348091269436,9.972572326660156,93.70565414428711,35127.301886792455,0.4436076585284145,0.0,30,40,46,51,57,68,85,94,94,94,94,94
GET,/ratings/,42,0,23,21.207468850272043,6.12950325012207,37.114858627319336,30028.0,0.3515381444942153,0.0,23,23,24,24,26,28,37,37,37,37,37,37
GET,/search/,54,0,17,15.807677198339391,3.0889511108398438,24.329662322998047,30164.0,0.45197761434970535,0.0,18,19,20,21,22,23,23,24,24,24,24,24
GET,/series/,44,0,20,19.72411437468095,5.149126052856445,28.244972229003906,29798.0,0.3682780561367969,0.0,21,23,23,24,25,26,28,28,28,28,28,28
GET,/tag/<id>,45,0,26,26.674440171983505,16.248226165771484,37.3835563659668,31041.577777777777,0.3766480119580878,0.0,26,27,28,29,30,35,37,37,37,37,37,37
GET,/tags/,45,0,32,29.666270150078667,3.5097599029541016,77.78573036193848,38073.0,0.3766480119580878,0.0,32,32,33,34,36,39,78,78,78,78,78,78
GET,search_by_author,55,0,22,22.336192564530805,12.028932571411133,51.23710632324219,30187.963636363635,0.4603475701709962,0.0,22,24,24,25,28,30,30,51,51,51,51,51
GET,search_by_identifier,45,0,26,24.89140298631456,17.200946807861328,34.82961654663086,30556.533333333333,0.3766480119580878,0.0,26,27,27,28,30,31,35,35,35,35,35,35
GET,search_by_title,43,0,26,24.867362754289495,12.270927429199219,32.17816352844238,30419.883720930233,0.35990810031550613,0.0,26,27,27,28,29,30,32,32,32,32,32,32
GET,search_generic,62,0,26,26.363442021031535,16.860246658325195,69.95797157287598,30512.548387096773,0.518937260920032,0.0,26,27,28,28,32,34,38,70,70,70,70,70
,Aggregated,814,3,26,37.75617238637563,3.0889511108398438,504.45032119750977,34674.45577395577,6.813144038530743,0.025109867463872518,26,28,31,32,43,130,310,330,500,500,500,500
1 Type Name Request Count Failure Count Median Response Time Average Response Time Min Response Time Max Response Time Average Content Size Requests/s Failures/s 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 99.999% 100%
2 GET /accounts/login/ 20 0 15 14.807796478271484 8.188486099243164 21.71945571899414 2024.0 0.16739911642581679 0.0 16 17 18 18 20 22 22 22 22 22 22 22
3 POST /accounts/login/ 20 0 320.0 335.94770431518555 302.3109436035156 504.45032119750977 76378.0 0.16739911642581679 0.0 330 340 340 350 370 500 500 500 500 500 500 500
4 GET /author/<id> 40 0 27 27.45213508605957 20.0350284576416 67.98744201660156 30708.725 0.33479823285163357 0.0 27 28 29 29 32 34 68 68 68 68 68 68
5 GET /authors/ 53 0 33 32.4410987350176 3.3960342407226562 151.21984481811523 39339.0 0.4436076585284145 0.0 33 33 35 36 43 47 73 150 150 150 150 150
6 GET /book/<id> 63 3 31 30.65679186866397 20.604372024536133 50.59003829956055 30451.74603174603 0.5273072167413229 0.025109867463872518 31 32 33 33 35 37 37 51 51 51 51 51
7 GET /books/ 42 0 130.0 110.17770994277228 3.3218860626220703 274.7330665588379 76378.0 0.3515381444942153 0.0 130 130 130 140 160 190 270 270 270 270 270 270
8 GET /publisher/<id> 48 0 27 26.85883641242981 17.744064331054688 69.56601142883301 30890.270833333332 0.4017578794219603 0.0 27 27 29 29 31 34 70 70 70 70 70 70
9 GET /publishers/ 40 0 24 22.84235954284668 4.957437515258789 34.004926681518555 33158.0 0.33479823285163357 0.0 24 26 27 27 28 31 34 34 34 34 34 34
10 GET /rating/<id> 53 0 30 36.589348091269436 9.972572326660156 93.70565414428711 35127.301886792455 0.4436076585284145 0.0 30 40 46 51 57 68 85 94 94 94 94 94
11 GET /ratings/ 42 0 23 21.207468850272043 6.12950325012207 37.114858627319336 30028.0 0.3515381444942153 0.0 23 23 24 24 26 28 37 37 37 37 37 37
12 GET /search/ 54 0 17 15.807677198339391 3.0889511108398438 24.329662322998047 30164.0 0.45197761434970535 0.0 18 19 20 21 22 23 23 24 24 24 24 24
13 GET /series/ 44 0 20 19.72411437468095 5.149126052856445 28.244972229003906 29798.0 0.3682780561367969 0.0 21 23 23 24 25 26 28 28 28 28 28 28
14 GET /tag/<id> 45 0 26 26.674440171983505 16.248226165771484 37.3835563659668 31041.577777777777 0.3766480119580878 0.0 26 27 28 29 30 35 37 37 37 37 37 37
15 GET /tags/ 45 0 32 29.666270150078667 3.5097599029541016 77.78573036193848 38073.0 0.3766480119580878 0.0 32 32 33 34 36 39 78 78 78 78 78 78
16 GET search_by_author 55 0 22 22.336192564530805 12.028932571411133 51.23710632324219 30187.963636363635 0.4603475701709962 0.0 22 24 24 25 28 30 30 51 51 51 51 51
17 GET search_by_identifier 45 0 26 24.89140298631456 17.200946807861328 34.82961654663086 30556.533333333333 0.3766480119580878 0.0 26 27 27 28 30 31 35 35 35 35 35 35
18 GET search_by_title 43 0 26 24.867362754289495 12.270927429199219 32.17816352844238 30419.883720930233 0.35990810031550613 0.0 26 27 27 28 29 30 32 32 32 32 32 32
19 GET search_generic 62 0 26 26.363442021031535 16.860246658325195 69.95797157287598 30512.548387096773 0.518937260920032 0.0 26 27 28 28 32 34 38 70 70 70 70 70
20 Aggregated 814 3 26 37.75617238637563 3.0889511108398438 504.45032119750977 34674.45577395577 6.813144038530743 0.025109867463872518 26 28 31 32 43 130 310 330 500 500 500 500

View File

@ -0,0 +1,62 @@
"Timestamp","User Count","Type","Name","Requests/s","Failures/s","50%","66%","75%","80%","90%","95%","98%","99%","99.9%","99.99%","99.999%","100%","Total Request Count","Total Failure Count","Total Median Response Time","Total Average Response Time","Total Min Response Time","Total Max Response Time","Total Average Content Size"
"1596336131","1","","Aggregated",0.00,0.00,"N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A",0,0,0,0,0,0,0
"1596336133","4","","Aggregated",0.00,0.00,18,19,300,300,330,330,330,330,330,330,330,330,12,0,18,115,8,333,37040
"1596336135","8","","Aggregated",6.00,0.00,19,22,300,320,330,330,340,340,340,340,340,340,26,0,19,110,8,344,36889
"1596336137","12","","Aggregated",6.00,0.00,22,28,300,320,330,350,370,370,370,370,370,370,45,0,22,104,3,373,37421
"1596336139","16","","Aggregated",7.33,0.00,23,28,190,310,330,350,370,370,370,370,370,370,63,0,23,99,3,373,37468
"1596336141","20","","Aggregated",7.14,0.00,23,27,51,310,330,340,370,370,370,370,370,370,80,0,23,94,3,373,36325
"1596336143","20","","Aggregated",8.00,0.00,26,30,51,270,330,350,370,500,500,500,500,500,96,0,24,91,3,504,36582
"1596336145","20","","Aggregated",8.30,0.00,26,30,32,40,310,340,370,500,500,500,500,500,111,0,26,82,3,504,35951
"1596336147","20","","Aggregated",8.60,0.00,27,30,32,35,270,340,370,500,500,500,500,500,127,0,25,76,3,504,35750
"1596336149","20","","Aggregated",7.90,0.00,27,31,33,36,130,270,340,500,500,500,500,500,140,0,26,73,3,504,35941
"1596336151","20","","Aggregated",7.90,0.00,27,31,34,35,44,130,150,240,240,240,240,240,153,0,26,72,3,504,35966
"1596336153","20","","Aggregated",7.10,0.00,28,33,35,36,78,130,150,240,240,240,240,240,168,0,26,69,3,504,36026
"1596336155","20","","Aggregated",7.30,0.00,28,33,36,36,78,130,150,240,240,240,240,240,177,0,26,67,3,504,35843
"1596336157","20","","Aggregated",7.30,0.00,28,34,36,36,78,130,150,240,240,240,240,240,188,1,26,64,3,504,35397
"1596336159","20","","Aggregated",6.50,0.10,26,32,35,36,78,140,150,240,240,240,240,240,205,1,26,62,3,504,35527
"1596336161","20","","Aggregated",6.50,0.10,26,29,33,35,47,73,130,140,140,140,140,140,218,1,26,60,3,504,35650
"1596336163","20","","Aggregated",6.60,0.10,25,27,29,32,35,68,140,160,160,160,160,160,235,1,26,58,3,504,35495
"1596336165","20","","Aggregated",6.60,0.10,26,28,32,33,42,130,140,160,160,160,160,160,244,1,26,57,3,504,35601
"1596336167","20","","Aggregated",6.70,0.10,26,29,32,33,55,130,130,160,160,160,160,160,255,2,26,56,3,504,35369
"1596336169","20","","Aggregated",6.60,0.10,26,29,32,32,43,58,130,160,160,160,160,160,269,2,26,54,3,504,35229
"1596336171","20","","Aggregated",6.40,0.10,26,28,31,32,39,55,58,130,130,130,130,130,284,2,26,53,3,504,35026
"1596336173","20","","Aggregated",6.70,0.10,26,29,31,32,39,58,130,130,130,130,130,130,296,2,26,52,3,504,35030
"1596336175","20","","Aggregated",6.10,0.10,26,29,31,32,35,55,130,130,130,130,130,130,310,2,26,51,3,504,34991
"1596336177","20","","Aggregated",6.40,0.10,25,29,30,31,35,51,130,130,130,130,130,130,320,2,26,50,3,504,34890
"1596336179","20","","Aggregated",6.50,0.00,25,28,29,30,33,120,130,130,130,130,130,130,336,2,26,49,3,504,34830
"1596336181","20","","Aggregated",6.70,0.00,26,27,29,30,33,44,130,140,140,140,140,140,351,2,26,48,3,504,34829
"1596336183","20","","Aggregated",6.10,0.00,25,27,29,30,35,94,130,140,140,140,140,140,363,2,26,48,3,504,34787
"1596336185","20","","Aggregated",6.70,0.00,26,28,29,30,37,120,150,190,190,190,190,190,378,2,26,48,3,504,34880
"1596336187","20","","Aggregated",6.40,0.00,26,28,30,33,85,130,150,190,190,190,190,190,391,2,26,47,3,504,34874
"1596336189","20","","Aggregated",6.80,0.00,27,29,33,34,94,140,150,190,190,190,190,190,400,2,26,47,3,504,34951
"1596336191","20","","Aggregated",6.60,0.00,27,28,32,33,46,130,150,190,190,190,190,190,417,2,26,46,3,504,34925
"1596336193","20","","Aggregated",6.90,0.00,26,28,29,31,37,85,130,130,130,130,130,130,430,2,26,46,3,504,34937
"1596336195","20","","Aggregated",6.60,0.00,26,29,30,32,46,120,130,130,130,130,130,130,443,2,26,45,3,504,34959
"1596336197","20","","Aggregated",6.60,0.00,26,28,29,30,35,46,120,130,130,130,130,130,454,2,26,45,3,504,34988
"1596336199","20","","Aggregated",6.50,0.00,26,28,29,30,32,37,120,130,130,130,130,130,473,2,26,44,3,504,34866
"1596336201","20","","Aggregated",7.10,0.00,26,28,29,30,32,35,120,130,130,130,130,130,485,2,26,44,3,504,34795
"1596336203","20","","Aggregated",6.80,0.00,26,29,30,30,32,33,34,38,38,38,38,38,501,2,26,43,3,504,34747
"1596336205","20","","Aggregated",7.20,0.00,27,29,30,31,32,33,34,43,43,43,43,43,512,2,26,43,3,504,34754
"1596336207","20","","Aggregated",7.10,0.00,27,28,30,32,33,40,56,130,130,130,130,130,529,2,26,42,3,504,34777
"1596336209","20","","Aggregated",7.20,0.00,27,29,32,33,40,57,130,130,130,130,130,130,541,2,26,42,3,504,34936
"1596336211","20","","Aggregated",6.80,0.00,27,29,32,33,43,130,130,130,130,130,130,130,554,2,26,42,3,504,34959
"1596336213","20","","Aggregated",7.10,0.00,27,29,32,33,57,130,130,130,130,130,130,130,572,2,26,42,3,504,35016
"1596336215","20","","Aggregated",6.90,0.00,27,28,31,33,57,130,130,130,130,130,130,130,579,2,26,42,3,504,34976
"1596336217","20","","Aggregated",6.80,0.00,27,28,31,32,50,130,130,130,130,130,130,130,594,2,26,41,3,504,34913
"1596336219","20","","Aggregated",6.70,0.00,27,27,29,31,45,120,130,140,140,140,140,140,613,2,26,41,3,504,34870
"1596336221","20","","Aggregated",6.70,0.00,26,27,28,29,32,46,130,140,140,140,140,140,625,2,26,41,3,504,34803
"1596336223","20","","Aggregated",6.80,0.00,25,27,28,30,32,33,50,140,140,140,140,140,638,2,26,40,3,504,34731
"1596336225","20","","Aggregated",6.80,0.00,25,27,30,31,32,33,50,140,140,140,140,140,648,2,26,40,3,504,34695
"1596336227","20","","Aggregated",6.80,0.00,26,27,31,31,33,34,36,140,140,140,140,140,663,2,26,40,3,504,34632
"1596336229","20","","Aggregated",6.70,0.00,26,28,31,32,34,36,36,44,44,44,44,44,673,2,26,40,3,504,34673
"1596336231","20","","Aggregated",6.60,0.00,26,29,31,33,35,36,44,120,120,120,120,120,687,2,26,39,3,504,34659
"1596336233","20","","Aggregated",6.40,0.00,27,29,31,33,35,44,68,120,120,120,120,120,698,2,26,39,3,504,34624
"1596336235","20","","Aggregated",5.90,0.00,26,28,30,32,35,56,120,120,120,120,120,120,714,2,26,39,3,504,34690
"1596336237","20","","Aggregated",6.30,0.00,26,28,30,32,44,120,120,130,130,130,130,130,724,2,26,39,3,504,34719
"1596336239","20","","Aggregated",6.00,0.00,24,27,28,30,34,56,120,130,130,130,130,130,737,2,26,39,3,504,34667
"1596336241","20","","Aggregated",6.20,0.00,24,27,28,29,34,68,120,130,130,130,130,130,750,2,26,38,3,504,34630
"1596336243","20","","Aggregated",6.20,0.00,24,26,27,28,31,36,58,130,130,130,130,130,765,2,26,38,3,504,34688
"1596336245","20","","Aggregated",6.90,0.00,24,26,28,28,32,36,58,130,130,130,130,130,778,2,26,38,3,504,34700
"1596336247","20","","Aggregated",6.50,0.00,25,27,28,30,34,37,58,130,130,130,130,130,790,3,26,38,3,504,34677
"1596336249","20","","Aggregated",6.70,0.10,25,27,30,31,34,37,58,130,130,130,130,130,802,3,26,38,3,504,34713
"1596336250","0","","Aggregated",6.10,0.10,24,27,29,30,33,37,58,130,130,130,130,130,814,3,26,37,3,504,34674
1 Timestamp User Count Type Name Requests/s Failures/s 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 99.999% 100% Total Request Count Total Failure Count Total Median Response Time Total Average Response Time Total Min Response Time Total Max Response Time Total Average Content Size
2 1596336131 1 Aggregated 0.00 0.00 N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A N/A 0 0 0 0 0 0 0
3 1596336133 4 Aggregated 0.00 0.00 18 19 300 300 330 330 330 330 330 330 330 330 12 0 18 115 8 333 37040
4 1596336135 8 Aggregated 6.00 0.00 19 22 300 320 330 330 340 340 340 340 340 340 26 0 19 110 8 344 36889
5 1596336137 12 Aggregated 6.00 0.00 22 28 300 320 330 350 370 370 370 370 370 370 45 0 22 104 3 373 37421
6 1596336139 16 Aggregated 7.33 0.00 23 28 190 310 330 350 370 370 370 370 370 370 63 0 23 99 3 373 37468
7 1596336141 20 Aggregated 7.14 0.00 23 27 51 310 330 340 370 370 370 370 370 370 80 0 23 94 3 373 36325
8 1596336143 20 Aggregated 8.00 0.00 26 30 51 270 330 350 370 500 500 500 500 500 96 0 24 91 3 504 36582
9 1596336145 20 Aggregated 8.30 0.00 26 30 32 40 310 340 370 500 500 500 500 500 111 0 26 82 3 504 35951
10 1596336147 20 Aggregated 8.60 0.00 27 30 32 35 270 340 370 500 500 500 500 500 127 0 25 76 3 504 35750
11 1596336149 20 Aggregated 7.90 0.00 27 31 33 36 130 270 340 500 500 500 500 500 140 0 26 73 3 504 35941
12 1596336151 20 Aggregated 7.90 0.00 27 31 34 35 44 130 150 240 240 240 240 240 153 0 26 72 3 504 35966
13 1596336153 20 Aggregated 7.10 0.00 28 33 35 36 78 130 150 240 240 240 240 240 168 0 26 69 3 504 36026
14 1596336155 20 Aggregated 7.30 0.00 28 33 36 36 78 130 150 240 240 240 240 240 177 0 26 67 3 504 35843
15 1596336157 20 Aggregated 7.30 0.00 28 34 36 36 78 130 150 240 240 240 240 240 188 1 26 64 3 504 35397
16 1596336159 20 Aggregated 6.50 0.10 26 32 35 36 78 140 150 240 240 240 240 240 205 1 26 62 3 504 35527
17 1596336161 20 Aggregated 6.50 0.10 26 29 33 35 47 73 130 140 140 140 140 140 218 1 26 60 3 504 35650
18 1596336163 20 Aggregated 6.60 0.10 25 27 29 32 35 68 140 160 160 160 160 160 235 1 26 58 3 504 35495
19 1596336165 20 Aggregated 6.60 0.10 26 28 32 33 42 130 140 160 160 160 160 160 244 1 26 57 3 504 35601
20 1596336167 20 Aggregated 6.70 0.10 26 29 32 33 55 130 130 160 160 160 160 160 255 2 26 56 3 504 35369
21 1596336169 20 Aggregated 6.60 0.10 26 29 32 32 43 58 130 160 160 160 160 160 269 2 26 54 3 504 35229
22 1596336171 20 Aggregated 6.40 0.10 26 28 31 32 39 55 58 130 130 130 130 130 284 2 26 53 3 504 35026
23 1596336173 20 Aggregated 6.70 0.10 26 29 31 32 39 58 130 130 130 130 130 130 296 2 26 52 3 504 35030
24 1596336175 20 Aggregated 6.10 0.10 26 29 31 32 35 55 130 130 130 130 130 130 310 2 26 51 3 504 34991
25 1596336177 20 Aggregated 6.40 0.10 25 29 30 31 35 51 130 130 130 130 130 130 320 2 26 50 3 504 34890
26 1596336179 20 Aggregated 6.50 0.00 25 28 29 30 33 120 130 130 130 130 130 130 336 2 26 49 3 504 34830
27 1596336181 20 Aggregated 6.70 0.00 26 27 29 30 33 44 130 140 140 140 140 140 351 2 26 48 3 504 34829
28 1596336183 20 Aggregated 6.10 0.00 25 27 29 30 35 94 130 140 140 140 140 140 363 2 26 48 3 504 34787
29 1596336185 20 Aggregated 6.70 0.00 26 28 29 30 37 120 150 190 190 190 190 190 378 2 26 48 3 504 34880
30 1596336187 20 Aggregated 6.40 0.00 26 28 30 33 85 130 150 190 190 190 190 190 391 2 26 47 3 504 34874
31 1596336189 20 Aggregated 6.80 0.00 27 29 33 34 94 140 150 190 190 190 190 190 400 2 26 47 3 504 34951
32 1596336191 20 Aggregated 6.60 0.00 27 28 32 33 46 130 150 190 190 190 190 190 417 2 26 46 3 504 34925
33 1596336193 20 Aggregated 6.90 0.00 26 28 29 31 37 85 130 130 130 130 130 130 430 2 26 46 3 504 34937
34 1596336195 20 Aggregated 6.60 0.00 26 29 30 32 46 120 130 130 130 130 130 130 443 2 26 45 3 504 34959
35 1596336197 20 Aggregated 6.60 0.00 26 28 29 30 35 46 120 130 130 130 130 130 454 2 26 45 3 504 34988
36 1596336199 20 Aggregated 6.50 0.00 26 28 29 30 32 37 120 130 130 130 130 130 473 2 26 44 3 504 34866
37 1596336201 20 Aggregated 7.10 0.00 26 28 29 30 32 35 120 130 130 130 130 130 485 2 26 44 3 504 34795
38 1596336203 20 Aggregated 6.80 0.00 26 29 30 30 32 33 34 38 38 38 38 38 501 2 26 43 3 504 34747
39 1596336205 20 Aggregated 7.20 0.00 27 29 30 31 32 33 34 43 43 43 43 43 512 2 26 43 3 504 34754
40 1596336207 20 Aggregated 7.10 0.00 27 28 30 32 33 40 56 130 130 130 130 130 529 2 26 42 3 504 34777
41 1596336209 20 Aggregated 7.20 0.00 27 29 32 33 40 57 130 130 130 130 130 130 541 2 26 42 3 504 34936
42 1596336211 20 Aggregated 6.80 0.00 27 29 32 33 43 130 130 130 130 130 130 130 554 2 26 42 3 504 34959
43 1596336213 20 Aggregated 7.10 0.00 27 29 32 33 57 130 130 130 130 130 130 130 572 2 26 42 3 504 35016
44 1596336215 20 Aggregated 6.90 0.00 27 28 31 33 57 130 130 130 130 130 130 130 579 2 26 42 3 504 34976
45 1596336217 20 Aggregated 6.80 0.00 27 28 31 32 50 130 130 130 130 130 130 130 594 2 26 41 3 504 34913
46 1596336219 20 Aggregated 6.70 0.00 27 27 29 31 45 120 130 140 140 140 140 140 613 2 26 41 3 504 34870
47 1596336221 20 Aggregated 6.70 0.00 26 27 28 29 32 46 130 140 140 140 140 140 625 2 26 41 3 504 34803
48 1596336223 20 Aggregated 6.80 0.00 25 27 28 30 32 33 50 140 140 140 140 140 638 2 26 40 3 504 34731
49 1596336225 20 Aggregated 6.80 0.00 25 27 30 31 32 33 50 140 140 140 140 140 648 2 26 40 3 504 34695
50 1596336227 20 Aggregated 6.80 0.00 26 27 31 31 33 34 36 140 140 140 140 140 663 2 26 40 3 504 34632
51 1596336229 20 Aggregated 6.70 0.00 26 28 31 32 34 36 36 44 44 44 44 44 673 2 26 40 3 504 34673
52 1596336231 20 Aggregated 6.60 0.00 26 29 31 33 35 36 44 120 120 120 120 120 687 2 26 39 3 504 34659
53 1596336233 20 Aggregated 6.40 0.00 27 29 31 33 35 44 68 120 120 120 120 120 698 2 26 39 3 504 34624
54 1596336235 20 Aggregated 5.90 0.00 26 28 30 32 35 56 120 120 120 120 120 120 714 2 26 39 3 504 34690
55 1596336237 20 Aggregated 6.30 0.00 26 28 30 32 44 120 120 130 130 130 130 130 724 2 26 39 3 504 34719
56 1596336239 20 Aggregated 6.00 0.00 24 27 28 30 34 56 120 130 130 130 130 130 737 2 26 39 3 504 34667
57 1596336241 20 Aggregated 6.20 0.00 24 27 28 29 34 68 120 130 130 130 130 130 750 2 26 38 3 504 34630
58 1596336243 20 Aggregated 6.20 0.00 24 26 27 28 31 36 58 130 130 130 130 130 765 2 26 38 3 504 34688
59 1596336245 20 Aggregated 6.90 0.00 24 26 28 28 32 36 58 130 130 130 130 130 778 2 26 38 3 504 34700
60 1596336247 20 Aggregated 6.50 0.00 25 27 28 30 34 37 58 130 130 130 130 130 790 3 26 38 3 504 34677
61 1596336249 20 Aggregated 6.70 0.10 25 27 30 31 34 37 58 130 130 130 130 130 802 3 26 38 3 504 34713
62 1596336250 0 Aggregated 6.10 0.10 24 27 29 30 33 37 58 130 130 130 130 130 814 3 26 37 3 504 34674

View File

@ -0,0 +1,22 @@
[
{
"pw": "insertpasswordhere",
"user": "insertuserhere1"
},
{
"pw": "insertpasswordhere",
"user": "insertuserhere2"
},
{
"pw": "insertpasswordhere",
"user": "insertuserhere3"
},
{
"pw": "insertpasswordhere",
"user": "insertuserhere4"
},
{
"pw": "insertpasswordhere",
"user": "insertuserhere5"
}
]

9
loadtesting/locust.conf Normal file
View File

@ -0,0 +1,9 @@
locustfile = locustfile.py
expect-workers = 200
host = http://localhost
users = 20
hatch-rate = 2
run-time = 2m
headless = true
csv=calibre
only-summary=true

187
loadtesting/locustfile.py Normal file
View File

@ -0,0 +1,187 @@
from locust import HttpUser, task, between
import random
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import json
# -------------------------------- fetching data to test with
with open("./../CalibreWebCompanion/settings.json", "r") as jfile:
calpath = json.load(jfile)["CALIBRE_DIR"] + "/metadata.db"
with open("dummyusers.json", "r") as jfile:
users = json.load(jfile)
engine = create_engine(f'sqlite:///{calpath}')
Base = declarative_base(engine)
class Author(Base): # needed
""""""
__tablename__ = 'authors'
__table_args__ = {'autoload': True}
# has int id, text name, sort
class Identifier(Base): # needed
""""""
__tablename__ = 'identifiers'
__table_args__ = {'autoload': True}
# has int id, int book, text value
class Publisher(Base): # needed
""""""
__tablename__ = 'publishers'
__table_args__ = {'autoload': True}
# has int id, text name
class Rating(Base): # needed
""""""
__tablename__ = 'ratings'
__table_args__ = {'autoload': True}
# has int id, int rating
class Series(Base): # needed
""""""
__tablename__ = 'series'
__table_args__ = {'autoload': True}
# has int id, text name
class Tag(Base): # needed
""""""
__tablename__ = 'tags'
__table_args__ = {'autoload': True}
# has int id, text name
class Book(Base): # needed
""""""
__tablename__ = 'books'
__table_args__ = {'autoload': True}
# has int id, text title, text sort, time timestamp, time pubdate,
# float series_index, text path
def loadSession():
""""""
metadata = Base.metadata
Session = sessionmaker(bind=engine)
session = Session()
return session
session = loadSession()
titles = [i.title for i in session.query(Book).all()]
authors = [i.name for i in session.query(Author).all()]
identifiers = [i.val for i in session.query(Identifier).all()]
book_ids = [i.id for i in session.query(Book).all()]
author_ids = [i.id for i in session.query(Author).all()]
publisher_ids = [i.id for i in session.query(Publisher).all()]
rating_ids = [i.id for i in session.query(Rating).all()]
series_ids = [i.id for i in session.query(Series).all()]
tag_ids = [i.id for i in session.query(Tag).all()]
def randlist(mylist):
return mylist[random.randint(0, len(mylist) - 1)]
class UserBehavior(HttpUser):
wait_time = between(1, 5)
def on_start(self):
""" on_start is called when a Locust start before any task is scheduled """
r = self.client.get('/accounts/login/')
self.client.headers['Referer'] = self.client.base_url
user = randlist(users)
self.client.post('/accounts/login/',
{
"username": user["user"],
"password": user["pw"],
'csrfmiddlewaretoken': r.cookies["csrftoken"]
})
@task(1)
def search_by_title(self):
title = randlist(titles)
self.client.get(f"/results/?title={title}", name="search_by_title")
@task(1)
def booklist(self):
self.client.get("/books/")
@task(1)
def bookdetail(self):
pk = randlist(book_ids)
self.client.get(f"/book/{pk}", name="/book/<id>")
@task(1)
def search_by_author(self):
author = randlist(authors)
self.client.get(f"/results/?author={author}", name="search_by_author")
@task(1)
def authorlist(self):
self.client.get("/authors/")
@task(1)
def authordetail(self):
pk = randlist(author_ids)
self.client.get(f"/author/{pk}", name="/author/<id>")
@task(1)
def search_by_id(self):
id_ = randlist(identifiers)
self.client.get(f"/results/?identifier={id_}", name="search_by_identifier")
@task(1)
def search_generic(self):
t = random.randint(0, 3)
if not t:
term = randlist(titles)
elif t == 1:
term = randlist(authors)
else:
term = randlist(identifiers)
self.client.get(f"/results/?generic={term}", name="search_generic")
@task(1)
def searchbad(self):
self.client.get("/search/")
@task(1)
def ratingslist(self):
self.client.get("/ratings/")
@task(1)
def ratingdetail(self):
pk = randlist(rating_ids)
self.client.get(f"/rating/{pk}", name="/rating/<id>")
@task(1)
def taglist(self):
self.client.get("/tags/")
@task(1)
def tagdetail(self):
pk = randlist(tag_ids)
self.client.get(f"/tag/{pk}", name="/tag/<id>")
@task(1)
def serieslist(self):
self.client.get("/series/")
@task(1)
def publisherlist(self):
self.client.get("/publishers/")
@task(1)
def publisherdetail(self):
pk = randlist(publisher_ids)
self.client.get(f"/publisher/{pk}", name="/publisher/<id>")

Binary file not shown.

View File

@ -1 +1,9 @@
django=3.0.8
django>=3.0.8
inotify>=0.2.10
gunicorn>=20.0
# development
# django-debug-toolbar>=2.2
# django-silk>=4.0
# locust>=1.1
# sqlalchemy>=1.3.15
# rich>=3.0

BIN
screenshots/bookdetail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
screenshots/booklist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
screenshots/navbar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
screenshots/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB