Compare commits
59 Commits
cfe706b1f5
...
shaquilles
Author | SHA1 | Date | |
---|---|---|---|
fd06a6c72e | |||
85fa11315b | |||
12d13635ad | |||
5fe4e94e73 | |||
e1c66fd034 | |||
7375ffe830 | |||
8ba82ef0db | |||
f7093e5e58 | |||
19c5b0830a | |||
23c1ff7140 | |||
8187817752 | |||
9160a37378 | |||
fd77792688 | |||
9f5e2e93dd | |||
3a2a2ce268 | |||
48443d9855 | |||
d7a385fd45 | |||
edc9366a5b | |||
88fcb17dc5 | |||
49ca4bccdc | |||
3985a5635d | |||
9843299ef6 | |||
75099ca05e | |||
1b8d81bd3c | |||
75e78d606f | |||
f9478f2894 | |||
b235f67be3 | |||
43e5d71cec | |||
e11ae55ed9 | |||
5182b2cdb6 | |||
6a2f89d36e | |||
ed03ea4a1c | |||
0806b55cbe | |||
10648a6d0a | |||
e62e54757a | |||
af1bfc06a5 | |||
d56911901b | |||
017e473b4d | |||
b65ef99935 | |||
aa3151dde6 | |||
53273fa3c2 | |||
2887fd852a | |||
acb2ab9e52 | |||
eca48c6cfa | |||
4e18118605 | |||
7a46de1679 | |||
398088454b | |||
9f0d46a17a | |||
071b82121c | |||
ac1d7fb5e8 | |||
915d8369bf | |||
1efa9b2166 | |||
777e949c9f | |||
32a2b3e6c1 | |||
14f14a2f33 | |||
d57f677bd3 | |||
dc1571bcfb | |||
5a83ccfc07 | |||
f13132d0c7 |
149
.gitignore
vendored
Normal file
149
.gitignore
vendored
Normal 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/
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -11,43 +11,99 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
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, ...)
|
# 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'
|
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)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
os.path.abspath(CALIBRE_DIR),
|
# os.path.abspath(CALIBRE_DIR),
|
||||||
# '/static/',
|
# '/static/',
|
||||||
]
|
]
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
STATIC_ROOT = BASE_DIR + "/static/"
|
||||||
# Quick-start development settings - unsuitable for production
|
## ##
|
||||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
#########################################################################
|
||||||
|
# LOGGING
|
||||||
# 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+'
|
|
||||||
|
|
||||||
|
|
||||||
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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
DEBUG_TOOLBAR_PANELS = [
|
DEBUG_TOOLBAR_PANELS = [
|
||||||
'debug_toolbar.panels.timer.TimerPanel',
|
'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'
|
LOGIN_REDIRECT_URL = '/books'
|
||||||
|
|
||||||
@ -78,11 +140,14 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
"library",
|
"library",
|
||||||
'debug_toolbar', # DEBUG purposes
|
# "silk", # DEBUG/profilling purposes
|
||||||
|
# 'debug_toolbar', # DEBUG purposes
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
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.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@ -90,7 +155,11 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django.middleware.cache.FetchFromCacheMiddleware', # cache
|
||||||
]
|
]
|
||||||
|
## ##
|
||||||
|
########################################################################
|
||||||
|
DEFAULT_CHARSET = "utf-8"
|
||||||
|
|
||||||
ROOT_URLCONF = 'CalibreWebCompanion.urls'
|
ROOT_URLCONF = 'CalibreWebCompanion.urls'
|
||||||
|
|
||||||
@ -115,14 +184,20 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'CalibreWebCompanion.wsgi.application'
|
WSGI_APPLICATION = 'CalibreWebCompanion.wsgi.application'
|
||||||
|
|
||||||
|
## ##
|
||||||
# Database
|
########################################################################
|
||||||
|
## DATBASE ##
|
||||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||||
|
|
||||||
|
if usersettings["ISDOCKER"]:
|
||||||
|
defaultdb_path = "calibre"
|
||||||
|
else:
|
||||||
|
defaultdb_path = BASE_DIR
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
'NAME': os.path.join(defaultdb_path, 'db.sqlite3'),
|
||||||
},
|
},
|
||||||
'calibre': {
|
'calibre': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'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
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||||
|
@ -22,6 +22,7 @@ from django.conf import settings
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('accounts/', include('django.contrib.auth.urls')),
|
path('accounts/', include('django.contrib.auth.urls')),
|
||||||
@ -30,8 +31,9 @@ urlpatterns = [
|
|||||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG: # DEBUG purposes
|
# if settings.DEBUG: # DEBUG purposes
|
||||||
import debug_toolbar
|
# urlpatterns+= [path('silk/', include('silk.urls', namespace='silk'))]
|
||||||
urlpatterns = [
|
# import debug_toolbar
|
||||||
path('__debug__/', include(debug_toolbar.urls)),
|
# urlpatterns = [
|
||||||
] + urlpatterns
|
# path('__debug__/', include(debug_toolbar.urls)),
|
||||||
|
# ] + urlpatterns
|
||||||
|
21
CalibreWebCompanion/CalibreWebCompanion/uwsgi.ini
Normal file
21
CalibreWebCompanion/CalibreWebCompanion/uwsgi.ini
Normal 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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,26 +1,51 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class DjangoRouter:
|
class DjangoRouter:
|
||||||
"""
|
"""
|
||||||
A router to control all database operations on models in the
|
A router to control all database operations on models in the
|
||||||
auth and contenttypes applications.
|
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):
|
def db_for_read(self, model, **hints):
|
||||||
"""
|
"""
|
||||||
Attempts to read auth and contenttypes models go to default.
|
Attempts to read auth and contenttypes models go to default.
|
||||||
"""
|
"""
|
||||||
if model._meta.app_label in self.route_app_labels:
|
if model._meta.app_label in self.route_app_labels:
|
||||||
return 'default'
|
return 'calibre'
|
||||||
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 None
|
return None
|
||||||
|
|
||||||
def allow_relation(self, obj1, obj2, **hints):
|
def allow_relation(self, obj1, obj2, **hints):
|
||||||
@ -35,42 +60,19 @@ class DjangoRouter:
|
|||||||
return True
|
return True
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
# 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
|
|
||||||
# """
|
# """
|
||||||
# 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):
|
# def db_for_write(self, model, **hints):
|
||||||
"""
|
|
||||||
Allow relations.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
# def allow_migrate(self, db, app_label, model_name=None, **hints): # might be prudent not to allow migrations
|
|
||||||
# """
|
# """
|
||||||
# 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
|
||||||
|
33
CalibreWebCompanion/gunicorn.conf.py
Normal file
33
CalibreWebCompanion/gunicorn.conf.py
Normal 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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +1,8 @@
|
|||||||
from .models import Author, Tag, Publisher, Language, Rating, Series
|
from .models import Author, Tag, Publisher, Language, Rating, Series
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def filters(request):
|
def filters(request):
|
||||||
# unique_authors = Author.objects.all().order_by('sort')
|
# 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_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_tags = Tag.objects.annotate(num_books=Count('book')).order_by('name')
|
||||||
unique_publishers = Publisher.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_ratings = Rating.objects.annotate(num_books=Count('book'))
|
||||||
unique_series = Series.objects.annotate(num_books=Count('book')).order_by('sort')
|
unique_series = Series.objects.annotate(num_books=Count('book')).order_by('sort')
|
||||||
|
|
||||||
@ -25,4 +27,4 @@ def filters(request):
|
|||||||
"unique_languages": unique_languages,
|
"unique_languages": unique_languages,
|
||||||
"unique_ratings": unique_ratings,
|
"unique_ratings": unique_ratings,
|
||||||
"unique_series": unique_series
|
"unique_series": unique_series
|
||||||
}
|
}
|
@ -1,12 +1,15 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class SearchForm(forms.Form):
|
class SearchForm(forms.Form):
|
||||||
title = forms.CharField(label="Title", max_length=200)
|
title = forms.CharField(label="Title", max_length=200)
|
||||||
author = forms.CharField(label='Author', max_length=100)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -7,7 +7,10 @@
|
|||||||
# Feel free to rename the models, but don't rename db_table values or field names.
|
# Feel free to rename the models, but don't rename db_table values or field names.
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(models.Model):
|
||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
@ -56,7 +59,7 @@ class Data(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Identifier(models.Model):
|
class Identifier(models.Model):
|
||||||
book = models.IntegerField()
|
book = models.ForeignKey("Book", db_column="book", on_delete=models.CASCADE)
|
||||||
type = models.TextField()
|
type = models.TextField()
|
||||||
val = models.TextField()
|
val = models.TextField()
|
||||||
|
|
||||||
@ -173,9 +176,9 @@ class Book(models.Model):
|
|||||||
title = models.TextField()
|
title = models.TextField()
|
||||||
sort = models.TextField(blank=True, null=True)
|
sort = models.TextField(blank=True, null=True)
|
||||||
# This field type is a guess.
|
# 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.
|
# This field type is a guess.
|
||||||
pubdate = models.TextField(blank=True, null=True)
|
pubdate = models.DateTimeField(blank=True, null=True)
|
||||||
series_index = models.FloatField()
|
series_index = models.FloatField()
|
||||||
author_sort = models.TextField(blank=True, null=True)
|
author_sort = models.TextField(blank=True, null=True)
|
||||||
isbn = models.TextField(blank=True, null=True)
|
isbn = models.TextField(blank=True, null=True)
|
||||||
@ -184,7 +187,7 @@ class Book(models.Model):
|
|||||||
flags = models.IntegerField()
|
flags = models.IntegerField()
|
||||||
uuid = models.TextField(blank=True, null=True)
|
uuid = models.TextField(blank=True, null=True)
|
||||||
has_cover = models.BooleanField(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(
|
authors = models.ManyToManyField(
|
||||||
Author,
|
Author,
|
||||||
through='BookAuthorLink',
|
through='BookAuthorLink',
|
||||||
@ -224,9 +227,9 @@ class Book(models.Model):
|
|||||||
through='BookRatingLink',
|
through='BookRatingLink',
|
||||||
through_fields=('book', 'rating'))
|
through_fields=('book', 'rating'))
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def rating(self):
|
def rating(self):
|
||||||
return self.rating.first()
|
return self.ratings.first()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
"""Returns the url to access a particular instance of MyModelName."""
|
"""Returns the url to access a particular instance of MyModelName."""
|
||||||
@ -381,26 +384,4 @@ class BookTagLink(models.Model):
|
|||||||
#
|
#
|
||||||
# class Meta:
|
# class Meta:
|
||||||
# managed = False
|
# managed = False
|
||||||
# db_table = 'feeds'
|
# 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'
|
|
@ -3,10 +3,8 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
{% block title %}<title>Local Library</title>{% endblock %}
|
{% block title %}<title>Local Library</title>{% endblock %}
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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>
|
<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">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<!-- Compiled and minified CSS -->
|
<!-- Compiled and minified CSS -->
|
||||||
@ -14,6 +12,16 @@
|
|||||||
<!-- Compiled and minified JavaScript -->
|
<!-- Compiled and minified JavaScript -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
|
||||||
<style>
|
<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 {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
@ -24,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
width: 40%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
@ -36,17 +44,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
width: 15%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.added {
|
.added {
|
||||||
width: 20%;
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.published {
|
||||||
|
width: 10%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="navbar-fixed">
|
<div class="navbar-fixed">
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-wrapper row green darken-1">
|
<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 class="active"><a href="{{user.get_absolute_url}}"> {{ user.get_username }}</a></li>
|
||||||
<li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
|
<li><a href="{% url 'logout'%}?next={{request.path}}">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
{% load cache %}
|
||||||
|
{% cache 500 sidebar request.user.username %}
|
||||||
|
<!--Maybe i'm retarded but this is not caching versions per user-->
|
||||||
<ul class="left">
|
<ul class="left">
|
||||||
<li><a href="{% url 'search' %}">Search</a></li>
|
|
||||||
<li><a href="{% url 'books' %}">Books</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>
|
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>
|
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>
|
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>
|
||||||
<ul id="dropdown-authors" class="dropdown-content">
|
<ul id="dropdown-authors" class="dropdown-content">
|
||||||
{% for author in unique_authors %}
|
{% for author in unique_authors %}
|
||||||
@ -79,16 +113,29 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<ul id="dropdown-tags" class="dropdown-content">
|
<ul id="dropdown-tags" class="dropdown-content">
|
||||||
{% for tag in unique_tags %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</ul>
|
</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 %}
|
{% else %}
|
||||||
<li><a href="{% url 'sign-up'%}?next={{request.path}}">Sign up</a></li>
|
<li><a href="{% url 'sign-up'%}?next={{request.path}}">Sign up</a></li>
|
||||||
<li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
|
<li><a href="{% url 'login'%}?next={{request.path}}">Login</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@ -99,7 +146,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% block content %} {% endblock %}
|
{% block content %} {% endblock %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -110,10 +156,11 @@
|
|||||||
<div class="col s12 m6 offset-m3">
|
<div class="col s12 m6 offset-m3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content center">
|
<div class="card-content center">
|
||||||
<p>You don't have permission to view this.</p>
|
<p>You don't have permission to view this.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action center">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
||||||
<td>{{book.author_sort}}</td>
|
<td>{{book.author_sort}}</td>
|
||||||
<td> {% for rating in book.ratings.all %}
|
<td>
|
||||||
|
{% for rating in book.ratings.all %}
|
||||||
{{rating}}
|
{{rating}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,51 +1,52 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% block title %}<title>{{book.title}}</title>{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<div class="col s12 m7">
|
<div class="col s12 m7">
|
||||||
<div class="card z-depth-0 horizontal">
|
<div class="card z-depth-0 horizontal">
|
||||||
<div class="card-image">
|
<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>
|
||||||
<div class="card-stacked">
|
<div class="card-stacked">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<h1> {{book.title}}</h1>
|
<h1> {{book.title}}</h1>
|
||||||
<h4> by
|
<h4> by
|
||||||
{% if book.authors %}
|
{% if book.authors %}
|
||||||
{% for author in book.authors.all %}
|
{% for author in book.authors.all %}
|
||||||
<a href="{{author.get_absolute_url}}">{{author.name}}</a>
|
<a href="{{author.get_absolute_url}}">{{author.name}}</a>
|
||||||
{%endfor%}
|
{%endfor%}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{book.author_sort}}
|
{{book.author_sort}}
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<br>
|
<br>
|
||||||
Published by
|
Published by
|
||||||
{% if book.publishers %}
|
{% if book.publishers %}
|
||||||
{% for pub in book.publishers.all %}
|
{% for pub in book.publishers.all %}
|
||||||
<a href="{{pub.get_absolute_url}}">{{pub.name}}</a>
|
<a href="{{pub.get_absolute_url}}">{{pub.name}}</a>
|
||||||
{%endfor%}
|
{%endfor%}
|
||||||
{% else %}
|
{% else %}
|
||||||
Unknown
|
Unknown
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<br>
|
<br>
|
||||||
Tags:
|
Tags:
|
||||||
{% if book.tags %}
|
{% if book.tags %}
|
||||||
{% for tag in book.tags.all %}
|
{% for tag in book.tags.all %}
|
||||||
<a href="{{tag.get_absolute_url}}">{{tag.name}}</a>,
|
<a href="{{tag.get_absolute_url}}">{{tag.name}}</a>,
|
||||||
{%endfor%}
|
{%endfor%}
|
||||||
{% else %}
|
{% else %}
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<br>
|
<br>
|
||||||
Rating:
|
Rating:
|
||||||
{% if book.ratings %}
|
{% if book.ratings %}
|
||||||
{% for rating in book.ratings.all %}
|
{% for rating in book.ratings.all %}
|
||||||
<a href="{{rating.get_absolute_url}}">{{rating}}</a>
|
<a href="{{rating.get_absolute_url}}">{{rating}}</a>
|
||||||
{%endfor%}
|
{%endfor%}
|
||||||
{% else %}
|
{% else %}
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<br>
|
<br>
|
||||||
<a href="{{book.publisher.get_absolute_url}}">{{book.publisher}}</a>
|
<a href="{{book.publisher.get_absolute_url}}">{{book.publisher}}</a>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,9 +54,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% autoescape off %}
|
{% autoescape off %}
|
||||||
{{comment}}
|
{{comment}}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -2,40 +2,67 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% 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>
|
<h1 class="center">Book List</h1>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col s1 m0">
|
<div class="col s1 m0">
|
||||||
</div>
|
</div>
|
||||||
<div class="col s10 m12">
|
<div class="col s10 m12">
|
||||||
<table id="books" class="highlight centered">
|
<table id="books" class="highlight centered">
|
||||||
<tr>
|
<tr>
|
||||||
<!--When a header is clicked, run the sortTable function, with a parameter, 0 for sorting by names, 1 for sorting by country:-->
|
<!--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="title" onclick="sortTable(0)">Title</th>
|
||||||
<th class="author" onclick="sortTable(1)">Author</th>
|
<th class="author" onclick="sortTable(1)">Author</th>
|
||||||
<th class="rating" onclick="sortTable(2)">Rating</th>
|
<th class="rating" onclick="sortTable(2)">Rating</th>
|
||||||
<th class="tags" onclick="sortTable(3)">Tags</th>
|
<th class="tags" onclick="sortTable(3)">Tags</th>
|
||||||
<th class="added" onclick="sortTable(4)">Added</th>
|
<th class="added" onclick="sortTable(4)">Added</th>
|
||||||
</tr>
|
<th class="published" onclick="sortTable(5)">Published</th>
|
||||||
{% for book in book_list %}
|
</tr>
|
||||||
<tr>
|
{% for book in book_list %}
|
||||||
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
<tr>
|
||||||
<td>{{book.author_sort}}</td>
|
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
||||||
<td> {% for rating in book.ratings.all %}
|
<td>{{book.author_sort}}</td>
|
||||||
{{rating}}
|
<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 %}
|
{% endfor %}
|
||||||
</td>
|
</table>
|
||||||
<td>
|
<div class="col s1 m0">
|
||||||
{% for tag in book.tags.all %}
|
</div>
|
||||||
{{tag}},
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>{{book.timestamp}}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
<div class="col s1 m0">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -18,7 +18,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
||||||
<td>{{book.author_sort}}</td>
|
<td>{{book.author_sort}}</td>
|
||||||
<td> {% for rating in book.rating.all %}
|
<td> {% for rating in book.ratings.all %}
|
||||||
{{rating}}
|
{{rating}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -17,9 +17,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
<td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
|
||||||
<td>{{book.author_sort}}</td>
|
<td>{{book.author_sort}}</td>
|
||||||
<td> {% for rating in book.rating.all %}
|
<td>
|
||||||
{{rating}}
|
{{rating}}
|
||||||
{% endfor %}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% for tag in book.tags.all %}
|
{% for tag in book.tags.all %}
|
||||||
|
@ -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 %}
|
@ -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 %}
|
@ -22,7 +22,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% for tag in book.tag.all %}
|
{% for tag in book.tags.all %}
|
||||||
{{tag}},
|
{{tag}},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
<input id="title" type="text" name="title" value="">
|
<input id="title" type="text" name="title" value="">
|
||||||
<label for="author">Author: </label>
|
<label for="author">Author: </label>
|
||||||
<input id="author" type="text" name="author" value="">
|
<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>
|
<button class="waves-effect waves-light btn green accent-4" type="submit">search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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()
|
@ -1,7 +1,9 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
from . import views
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('authors/', views.AuthorListView.as_view(), name='authors'),
|
path('authors/', views.AuthorListView.as_view(), name='authors'),
|
||||||
@ -9,17 +11,21 @@ urlpatterns = [
|
|||||||
path('publishers/', views.PublisherListView.as_view(), name='publishers'),
|
path('publishers/', views.PublisherListView.as_view(), name='publishers'),
|
||||||
path('ratings/', views.RatingListView.as_view(), name='ratings'),
|
path('ratings/', views.RatingListView.as_view(), name='ratings'),
|
||||||
path('tags/', views.TagListView.as_view(), name='tags'),
|
path('tags/', views.TagListView.as_view(), name='tags'),
|
||||||
|
path('series/', views.SeriesListView.as_view(), name='series'),
|
||||||
path('author/<int:pk>', views.AuthorDetailView.as_view(), name='author-detail-view'),
|
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('book/<int:pk>', views.BookDetailView.as_view(), name='book-detail-view'),
|
||||||
path('publisher/<int:pk>', views.PublisherDetailView.as_view(), name='publisher-detail-view'),
|
path('publisher/<int:pk>', views.PublisherDetailView.as_view(),
|
||||||
path('rating/<int:pk>', views.RatingDetailView.as_view(), name='rating-detail-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('tag/<int:pk>', views.TagDetailView.as_view(), name='tag-detail-view'),
|
||||||
|
|
||||||
path('results/', views.ResultsView.as_view(), name='results'),
|
path('results/', views.ResultsView.as_view(), name='results'),
|
||||||
path('search/', views.SearchView.as_view(), name='search'),
|
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")
|
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
from django.utils.decorators import method_decorator
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import generic
|
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 django.http import HttpResponseRedirect
|
||||||
from .forms import SearchForm, UserCreationForm
|
from .forms import SearchForm, UserCreationForm
|
||||||
from django.db import models
|
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.models import User
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# might be helpful for vary headers later
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -30,6 +36,9 @@ def sign_up(request):
|
|||||||
class SearchView(generic.TemplateView):
|
class SearchView(generic.TemplateView):
|
||||||
template_name = 'search.html'
|
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.
|
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
|
# 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
|
model = Book
|
||||||
template_name = 'results.html'
|
template_name = 'results.html'
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(ResultsView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self): # new
|
def get_queryset(self): # new
|
||||||
title = self.request.GET.get('title')
|
title = self.request.GET.get('title')
|
||||||
author = self.request.GET.get('author')
|
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")
|
books = Book.objects.prefetch_related("tags", "ratings")
|
||||||
if title:
|
if title:
|
||||||
books =books.filter(sort__icontains=title)
|
books = books.filter(sort__icontains=title)
|
||||||
if author:
|
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
|
return books
|
||||||
|
|
||||||
|
|
||||||
class AuthorListView(generic.ListView):
|
class AuthorListView(generic.ListView):
|
||||||
model = Author
|
model = Author
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(AuthorListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BookListView(generic.ListView):
|
class BookListView(generic.ListView):
|
||||||
model = Book
|
model = Book
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(BookListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Annotate the books with ratings, tags, etc
|
# Annotate the books with ratings, tags, etc
|
||||||
# books = Book.objects.annotate(
|
# books = Book.objects.annotate(
|
||||||
@ -64,18 +109,37 @@ class BookListView(generic.ListView):
|
|||||||
class PublisherListView(generic.ListView):
|
class PublisherListView(generic.ListView):
|
||||||
model = Publisher
|
model = Publisher
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(PublisherListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RatingListView(generic.ListView):
|
class RatingListView(generic.ListView):
|
||||||
model = Rating
|
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):
|
class TagListView(generic.ListView):
|
||||||
model = Tag
|
model = Tag
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(TagListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AuthorDetailView(generic.DetailView):
|
class AuthorDetailView(generic.DetailView):
|
||||||
model = Author
|
model = Author
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(AuthorDetailView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# Call the base implementation first to get the context
|
# Call the base implementation first to get the context
|
||||||
context = super(AuthorDetailView, self).get_context_data(**kwargs)
|
context = super(AuthorDetailView, self).get_context_data(**kwargs)
|
||||||
@ -89,6 +153,9 @@ class AuthorDetailView(generic.DetailView):
|
|||||||
class BookDetailView(generic.DetailView):
|
class BookDetailView(generic.DetailView):
|
||||||
model = Book
|
model = Book
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(BookDetailView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# Call the base implementation first to get the context
|
# Call the base implementation first to get the context
|
||||||
context = super(BookDetailView, self).get_context_data(**kwargs)
|
context = super(BookDetailView, self).get_context_data(**kwargs)
|
||||||
@ -100,13 +167,16 @@ class BookDetailView(generic.DetailView):
|
|||||||
pass
|
pass
|
||||||
context["imgpath"] = context["object"].path + "/cover.jpg"
|
context["imgpath"] = context["object"].path + "/cover.jpg"
|
||||||
download = Data.objects.get(book=context["object"].id)
|
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
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PublisherDetailView(generic.DetailView):
|
class PublisherDetailView(generic.DetailView):
|
||||||
model = Publisher
|
model = Publisher
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(PublisherDetailView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# Call the base implementation first to get the context
|
# Call the base implementation first to get the context
|
||||||
context = super(PublisherDetailView, self).get_context_data(**kwargs)
|
context = super(PublisherDetailView, self).get_context_data(**kwargs)
|
||||||
@ -120,6 +190,9 @@ class PublisherDetailView(generic.DetailView):
|
|||||||
class RatingDetailView(generic.DetailView):
|
class RatingDetailView(generic.DetailView):
|
||||||
model = Rating
|
model = Rating
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(RatingDetailView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# Call the base implementation first to get the context
|
# Call the base implementation first to get the context
|
||||||
context = super(RatingDetailView, self).get_context_data(**kwargs)
|
context = super(RatingDetailView, self).get_context_data(**kwargs)
|
||||||
@ -133,6 +206,9 @@ class RatingDetailView(generic.DetailView):
|
|||||||
class TagDetailView(generic.DetailView):
|
class TagDetailView(generic.DetailView):
|
||||||
model = Tag
|
model = Tag
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(TagDetailView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# Call the base implementation first to get the context
|
# Call the base implementation first to get the context
|
||||||
context = super(TagDetailView, self).get_context_data(**kwargs)
|
context = super(TagDetailView, self).get_context_data(**kwargs)
|
||||||
@ -141,3 +217,19 @@ class TagDetailView(generic.DetailView):
|
|||||||
books = books.filter(tags=context["object"].id)
|
books = books.filter(tags=context["object"].id)
|
||||||
context['books'] = sorted(books, key=lambda x: x.title)
|
context['books'] = sorted(books, key=lambda x: x.title)
|
||||||
return context
|
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
0
CalibreWebCompanion/manage.py
Normal file → Executable file
15
CalibreWebCompanion/settings.json
Normal file
15
CalibreWebCompanion/settings.json
Normal 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
35
Dockerfile
Normal 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"]
|
65
README.md
65
README.md
@ -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
|
||||||
|

|
||||||
|
Book detail
|
||||||
|

|
||||||
|
navbar
|
||||||
|

|
||||||
|
Adanced search
|
||||||
|

|
||||||
# requirements
|
# requirements
|
||||||
Django 3.0
|
Django 3.0
|
||||||
|
Calibre 4.13 (I have not tested it with anything else atm, will be resolved later)
|
||||||
|
|
||||||
# how to use:
|
# how to use:
|
||||||
Edit `./CalibreWebCompanion/CalibreWebCompanion/settings`.
|
1. [Docker setup](./deployment/instructions.md#user-content-docker-detup)
|
||||||
Set CALIBREPATH to the path of your library
|
2. [Non Docker setup](./deployment/instructions.md#user-content-non-docker-detup)
|
||||||
`./CalibreWebCompanion`
|
|
||||||
run `./manage.py runserver`
|
|
||||||
|
|
||||||
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] Books
|
||||||
- [x] navbar with tags, series, authors, etc
|
- [x] navbar with tags, series, authors, etc
|
||||||
- [x] Search
|
- [x] Search
|
||||||
- [x] authentication
|
- [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
|
|
||||||
|
|
29
deployment/Dockerfile
Normal file
29
deployment/Dockerfile
Normal 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
4
deployment/deploy.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from os import environ
|
||||||
|
|
||||||
|
|
||||||
|
|
20
deployment/docker/nginx.conf
Normal file
20
deployment/docker/nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
4
deployment/docker/start.sh
Normal file
4
deployment/docker/start.sh
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
uwsgi --ini CalibreWebCompanion/CalibreWebCompanion/uwsgi.ini
|
||||||
|
nginx -g 'daemon off;'
|
45
deployment/instructions.md
Normal file
45
deployment/instructions.md
Normal 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
81
deployment/nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
deployment/startupscript.py
Normal file
11
deployment/startupscript.py
Normal 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")
|
||||||
|
|
36
deployment/supervisord.conf
Normal file
36
deployment/supervisord.conf
Normal 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
34
loadtesting/bench.py
Normal 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)
|
2
loadtesting/calibre_failures.csv
Normal file
2
loadtesting/calibre_failures.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Method,Name,Error,Occurrences
|
||||||
|
GET,/book/<id>,500 Server Error: Internal Server Error for url: /book/<id>,3
|
|
20
loadtesting/calibre_stats.csv
Normal file
20
loadtesting/calibre_stats.csv
Normal 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
|
|
62
loadtesting/calibre_stats_history.csv
Normal file
62
loadtesting/calibre_stats_history.csv
Normal 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
|
|
22
loadtesting/dummyusers.json.bak
Normal file
22
loadtesting/dummyusers.json.bak
Normal 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
9
loadtesting/locust.conf
Normal 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
187
loadtesting/locustfile.py
Normal 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>")
|
BIN
models.vsdx
BIN
models.vsdx
Binary file not shown.
@ -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
BIN
screenshots/bookdetail.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 288 KiB |
BIN
screenshots/booklist.png
Normal file
BIN
screenshots/booklist.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
BIN
screenshots/navbar.png
Normal file
BIN
screenshots/navbar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
BIN
screenshots/search.png
Normal file
BIN
screenshots/search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user