Compare commits
	
		
			62 Commits
		
	
	
		
			ee7a0bfc32
			...
			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 | ||
| 
						 | 
					cfe706b1f5 | ||
| 
						 | 
					5d522da15e | ||
| 
						 | 
					58d4f27c61 | 
							
								
								
									
										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\\stefa\\Downloads\\db")
 | 
					 | 
				
			||||||
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",
 | 
				
			||||||
 | 
					    # "silk", # DEBUG/profilling purposes
 | 
				
			||||||
    # 'debug_toolbar', # DEBUG  purposes
 | 
					    # 'debug_toolbar', # DEBUG  purposes
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MIDDLEWARE = [
 | 
					MIDDLEWARE = [
 | 
				
			||||||
 | 
					    # 'silk.middleware.SilkyMiddleware', # DEBUG/profiling purposes
 | 
				
			||||||
    # 'debug_toolbar.middleware.DebugToolbarMiddleware', # DEBUG 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')),
 | 
				
			||||||
@@ -31,7 +32,8 @@ urlpatterns = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 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.
										
									
								
							
							
								
								
									
										107
									
								
								CalibreWebCompanion/library/calibredb.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								CalibreWebCompanion/library/calibredb.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					from sqlalchemy import create_engine
 | 
				
			||||||
 | 
					from sqlalchemy.ext.declarative import declarative_base
 | 
				
			||||||
 | 
					from sqlalchemy.orm import sessionmaker
 | 
				
			||||||
 | 
					engine = create_engine('sqlite:///C://Users//MassiveAtoms//Documents//Calibre Library//metadata.db.', echo=True)
 | 
				
			||||||
 | 
					Base = declarative_base(engine)
 | 
				
			||||||
 | 
					########################################################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Author(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'authors'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id, text name, sort 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Comment(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'comments'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id, int book, text text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# class Data(Base): # maybe
 | 
				
			||||||
 | 
					#     """"""
 | 
				
			||||||
 | 
					#     __tablename__ = 'data'
 | 
				
			||||||
 | 
					#     __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					#     # has int id, int book, text format, text name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Book_author_link(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'books_authors_link'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id,  id book, id author
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Book_publisher_link(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'books_publishers_link'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id,  id book, id publisher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Book_rating_link(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'books_ratings_link'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id,  id book, id rating
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Book_series_link(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'books_series_link'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id,  id book, id series
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Book_tags_link(Base): # needed
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    __tablename__ = 'books_tags_link'
 | 
				
			||||||
 | 
					    __table_args__ = {'autoload':True}
 | 
				
			||||||
 | 
					    # has int id,  id book, id tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#----------------------------------------------------------------------
 | 
				
			||||||
 | 
					def loadSession():
 | 
				
			||||||
 | 
					    """"""
 | 
				
			||||||
 | 
					    metadata = Base.metadata
 | 
				
			||||||
 | 
					    Session = sessionmaker(bind=engine)
 | 
				
			||||||
 | 
					    session = Session()
 | 
				
			||||||
 | 
					    return session
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    session = loadSession()
 | 
				
			||||||
 | 
					    res = session.query(Book).all()
 | 
				
			||||||
 | 
					    for i in res:
 | 
				
			||||||
 | 
					        print(i.id, i.title, i.sort, i.timestamp, i.pubdate, i.series_index, i.path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,13 +1,24 @@
 | 
				
			|||||||
from .models import Author, Tag, Publisher, Language, Rating, Series
 | 
					from .models import Author, Tag, Publisher, Language, Rating, Series
 | 
				
			||||||
 | 
					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')
 | 
				
			||||||
    unique_tags = Tag.objects.all().order_by('name')
 | 
					    # unique_tags = Tag.objects.all().order_by('name')
 | 
				
			||||||
    unique_publishers = Publisher.objects.all().order_by('name')
 | 
					    # unique_publishers = Publisher.objects.all().order_by('name')
 | 
				
			||||||
    unique_languages = Language.objects.all()
 | 
					    # unique_ratings = Rating.objects.all().order_by('rating')
 | 
				
			||||||
    unique_ratings = Rating.objects.all().order_by('rating')
 | 
					    # unique_languages = Language.objects.all()
 | 
				
			||||||
    unique_series = Series.objects.all().order_by('sort')
 | 
					    # unique_series = Series.objects.all().order_by('sort')
 | 
				
			||||||
 | 
					    # unique_authors = Author.objects.annotate(num_books=Count('book')).order_by('sort')
 | 
				
			||||||
 | 
					    unique_authors = Author.objects.only('name', "id").annotate(num_books=Count('book')).order_by('name')
 | 
				
			||||||
 | 
					    unique_tags = Tag.objects.annotate(num_books=Count('book')).order_by('name')
 | 
				
			||||||
 | 
					    unique_publishers = Publisher.objects.annotate(num_books=Count('book')).order_by('name')
 | 
				
			||||||
 | 
					    unique_languages = Language.objects.annotate(num_books=Count('book')).order_by('lang_code')
 | 
				
			||||||
 | 
					    unique_ratings = Rating.objects.annotate(num_books=Count('book'))
 | 
				
			||||||
 | 
					    unique_series = Series.objects.annotate(num_books=Count('book')).order_by('sort')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        "unique_authors": unique_authors,
 | 
					        "unique_authors": unique_authors,
 | 
				
			||||||
@@ -16,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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,221 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.0.7 on 2020-07-11 16:22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('library', '0001_initial'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Author',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.TextField()),
 | 
				
			||||||
 | 
					                ('sort', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('link', models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'authors',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Book',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('title', models.TextField()),
 | 
				
			||||||
 | 
					                ('sort', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('timestamp', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('pubdate', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('series_index', models.FloatField()),
 | 
				
			||||||
 | 
					                ('author_sort', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('isbn', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('lccn', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('path', models.TextField()),
 | 
				
			||||||
 | 
					                ('flags', models.IntegerField()),
 | 
				
			||||||
 | 
					                ('uuid', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('has_cover', models.BooleanField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('last_modified', models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='BookAuthorLink',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books_authors_link',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='BookLanguageLink',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('item_order', models.IntegerField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books_languages_link',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='BookPublisherLink',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books_publishers_link',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='BookRatingLink',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books_ratings_link',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='BookSeriesLink',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books_series_link',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='BookTagLink',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'books_tags_link',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Comment',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('text', models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'comments',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Identifier',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('book', models.IntegerField()),
 | 
				
			||||||
 | 
					                ('type', models.TextField()),
 | 
				
			||||||
 | 
					                ('val', models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'identifiers',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Language',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('lang_code', models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'languages',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Publisher',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.TextField()),
 | 
				
			||||||
 | 
					                ('sort', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'publishers',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Rating',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('rating', models.IntegerField(blank=True, null=True)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'ratings',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Tag',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'db_table': 'tags',
 | 
				
			||||||
 | 
					                'managed': False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Authors',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Books',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BooksAuthorsLink',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BooksLanguagesLink',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BooksPublishersLink',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BooksRatingsLink',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BooksSeriesLink',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BooksTagsLink',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Comments',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Identifiers',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Languages',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Publishers',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Ratings',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='Tags',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
										
											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()
 | 
				
			||||||
@@ -17,7 +20,7 @@ class Author(models.Model):
 | 
				
			|||||||
    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."""
 | 
				
			||||||
        return reverse('author-detail-view', args=[str(self.id)])
 | 
					        return reverse('author-detail-view', args=[str(self.id)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
@@ -27,12 +30,11 @@ class Author(models.Model):
 | 
				
			|||||||
        db_table = 'authors'
 | 
					        db_table = 'authors'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class Comment(models.Model):
 | 
					class Comment(models.Model):
 | 
				
			||||||
    book = models.ForeignKey("Book", db_column="book", on_delete=models.CASCADE)
 | 
					    book = models.ForeignKey("Book", db_column="book",
 | 
				
			||||||
 | 
					                             on_delete=models.CASCADE)
 | 
				
			||||||
    text = models.TextField()
 | 
					    text = models.TextField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        managed = False
 | 
					        managed = False
 | 
				
			||||||
        db_table = 'comments'
 | 
					        db_table = 'comments'
 | 
				
			||||||
@@ -41,7 +43,6 @@ class Comment(models.Model):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class Data(models.Model):
 | 
					class Data(models.Model):
 | 
				
			||||||
    book = models.IntegerField()
 | 
					    book = models.IntegerField()
 | 
				
			||||||
    format = models.TextField()
 | 
					    format = models.TextField()
 | 
				
			||||||
@@ -58,10 +59,10 @@ 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()
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.val
 | 
					        return self.val
 | 
				
			||||||
@@ -77,7 +78,7 @@ class Language(models.Model):
 | 
				
			|||||||
    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."""
 | 
				
			||||||
        return reverse('language-detail-view', args=[str(self.lang_code)])
 | 
					        return reverse('language-detail-view', args=[str(self.lang_code)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.lang_code
 | 
					        return self.lang_code
 | 
				
			||||||
@@ -98,12 +99,12 @@ class Publisher(models.Model):
 | 
				
			|||||||
        through='BookPublisherLink',
 | 
					        through='BookPublisherLink',
 | 
				
			||||||
        through_fields=('publisher', 'book'),
 | 
					        through_fields=('publisher', 'book'),
 | 
				
			||||||
        related_name="released"
 | 
					        related_name="released"
 | 
				
			||||||
        )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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."""
 | 
				
			||||||
        return reverse('publisher-detail-view', args=[str(self.id)])
 | 
					        return reverse('publisher-detail-view', args=[str(self.id)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
@@ -118,10 +119,11 @@ class Publisher(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Rating(models.Model):
 | 
					class Rating(models.Model):
 | 
				
			||||||
    rating = models.IntegerField(blank=True, null=True)
 | 
					    rating = models.IntegerField(blank=True, null=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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."""
 | 
				
			||||||
        return reverse('rating-detail-view', args=[str(self.id)])
 | 
					        return reverse('rating-detail-view', args=[str(self.id)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return str(self.rating)
 | 
					        return str(self.rating)
 | 
				
			||||||
@@ -138,7 +140,7 @@ class Series(models.Model):
 | 
				
			|||||||
    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."""
 | 
				
			||||||
        return reverse('series-detail-view', args=[str(self.id)])
 | 
					        return reverse('series-detail-view', args=[str(self.id)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
@@ -153,10 +155,11 @@ class Series(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Tag(models.Model):
 | 
					class Tag(models.Model):
 | 
				
			||||||
    name = models.TextField()
 | 
					    name = models.TextField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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."""
 | 
				
			||||||
        return reverse('tag-detail-view', args=[str(self.id)])
 | 
					        return reverse('tag-detail-view', args=[str(self.id)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
@@ -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',
 | 
				
			||||||
@@ -193,14 +196,28 @@ class Book(models.Model):
 | 
				
			|||||||
        Language,
 | 
					        Language,
 | 
				
			||||||
        through='BookLanguageLink',
 | 
					        through='BookLanguageLink',
 | 
				
			||||||
        through_fields=('book', 'lang_code'))
 | 
					        through_fields=('book', 'lang_code'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def language(self):
 | 
				
			||||||
 | 
					        return self.languages.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    publishers = models.ManyToManyField(
 | 
					    publishers = models.ManyToManyField(
 | 
				
			||||||
        Publisher,
 | 
					        Publisher,
 | 
				
			||||||
        through='BookPublisherLink',
 | 
					        through='BookPublisherLink',
 | 
				
			||||||
        through_fields=('book', 'publisher'))
 | 
					        through_fields=('book', 'publisher'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def publisher(self):
 | 
				
			||||||
 | 
					        return self.publishers.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    series = models.ManyToManyField(
 | 
					    series = models.ManyToManyField(
 | 
				
			||||||
        Series,
 | 
					        Series,
 | 
				
			||||||
        through='BookSeriesLink',
 | 
					        through='BookSeriesLink',
 | 
				
			||||||
        through_fields=('book', 'series'))
 | 
					        through_fields=('book', 'series'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def serie(self):
 | 
				
			||||||
 | 
					        return self.series.first()
 | 
				
			||||||
    tags = models.ManyToManyField(
 | 
					    tags = models.ManyToManyField(
 | 
				
			||||||
        Tag,
 | 
					        Tag,
 | 
				
			||||||
        through='BookTagLink',
 | 
					        through='BookTagLink',
 | 
				
			||||||
@@ -209,10 +226,15 @@ class Book(models.Model):
 | 
				
			|||||||
        Rating,
 | 
					        Rating,
 | 
				
			||||||
        through='BookRatingLink',
 | 
					        through='BookRatingLink',
 | 
				
			||||||
        through_fields=('book', 'rating'))
 | 
					        through_fields=('book', 'rating'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @cached_property
 | 
				
			||||||
 | 
					    def rating(self):
 | 
				
			||||||
 | 
					        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."""
 | 
				
			||||||
        return reverse('book-detail-view', args=[str(self.id)])
 | 
					        return reverse('book-detail-view', args=[str(self.id)])
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
					        """String for representing the MyModelName object (in Admin site etc.)."""
 | 
				
			||||||
        return self.title
 | 
					        return self.title
 | 
				
			||||||
@@ -362,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 %}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								CalibreWebCompanion/library/templates/library/results.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								CalibreWebCompanion/library/templates/library/results.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					{% load static %}
 | 
				
			||||||
 | 
					<h1>Results</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<table id="books">
 | 
				
			||||||
 | 
					  <tr>
 | 
				
			||||||
 | 
					    <!--When a header is clicked, run the sortTable function, with a parameter, 0 for sorting by names, 1 for sorting by country:-->
 | 
				
			||||||
 | 
					    <th onclick="sortTable(0)">Title</th>
 | 
				
			||||||
 | 
					    <th onclick="sortTable(1)">Author</th>
 | 
				
			||||||
 | 
					    <th onclick="sortTable(2)">Rating</th>
 | 
				
			||||||
 | 
					    <th onclick="sortTable(3)">Tags</th>
 | 
				
			||||||
 | 
					    <th onclick="sortTable(4)">Added</th>
 | 
				
			||||||
 | 
					  </tr>
 | 
				
			||||||
 | 
					  {% for book in book_list %}
 | 
				
			||||||
 | 
					  <tr>
 | 
				
			||||||
 | 
					    <td><a href="{{ book.get_absolute_url }}">{{ book.title }}</a></td>
 | 
				
			||||||
 | 
					    <td>{{book.author_sort}}</td>
 | 
				
			||||||
 | 
					    <td> {% for rating in book.ratings.all %}
 | 
				
			||||||
 | 
					      {{rating}}
 | 
				
			||||||
 | 
					      {% endfor %}
 | 
				
			||||||
 | 
					    </td>
 | 
				
			||||||
 | 
					    <td>
 | 
				
			||||||
 | 
					      {% for tag in book.tags.all %}
 | 
				
			||||||
 | 
					      {{tag}},
 | 
				
			||||||
 | 
					      {% endfor %}
 | 
				
			||||||
 | 
					    </td>
 | 
				
			||||||
 | 
					    <td>{{book.timestamp}}</td>
 | 
				
			||||||
 | 
					  </tr>
 | 
				
			||||||
 | 
					  {% endfor %}
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
@@ -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"]
 | 
				
			||||||
							
								
								
									
										71
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										71
									
								
								README.md
									
									
									
									
									
								
							@@ -1,10 +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.  
 | 
					
 | 
				
			||||||
 | 
					# Ignore pretty much everything below if you're not working on the project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Profiling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To do profiling, you have to create some dummy users  
 | 
				
			||||||
 | 
					Unbakify a file `./loadtesting/dummyusers.json.bak`  and fill in the credentials for the dummy users  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					While django is running, open another shell and cd to `./loadtesting` and run `./bench.py`
 | 
				
			||||||
 | 
					To have a more interactive session, 
 | 
				
			||||||
 | 
					comment out 
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					run-time = 2m
 | 
				
			||||||
 | 
					headless = true
 | 
				
			||||||
 | 
					``` 
 | 
				
			||||||
 | 
					in `locust.conf`, and then run `./bench.py`
 | 
				
			||||||
 | 
					You can then go to [http://localhost:8089/](http://localhost:8089/) to see live graphs, tweak the number of users and more.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Finished Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [x] Books
 | 
				
			||||||
 | 
					- [x] navbar with tags, series, authors, etc
 | 
				
			||||||
 | 
					- [x] Search
 | 
				
			||||||
 | 
					- [x] authentication 
 | 
				
			||||||
 | 
					- [x] Cache 
 | 
				
			||||||
 | 
					- [x] logging
 | 
				
			||||||
 | 
					- [x]  deploy instructions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO ROADMAP
 | 
				
			||||||
 | 
					- [ ] cache with vary headers
 | 
				
			||||||
 | 
					- [ ] localisation
 | 
				
			||||||
 | 
					- [ ] Beautifying template (only works well on 720p, no other viewports)
 | 
				
			||||||
 | 
					- [ ]  Setup email functionality (atm, there's only a dummy one, and you can't reset passwords)
 | 
				
			||||||
 | 
					- [ ]  isolate the styling and templates, so we can swap them out by just swapping directory content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
							
								
								
									
										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