diff --git a/.gitignore b/.gitignore index 20adfe1..765a1e4 100755 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ uwsgi.ini installer.log pyshelf_nginx.conf !docker/pyshelf_nginx.conf +.env tags TAGS config.json diff --git a/config.json b/config.json index a1d7465..56985a6 100755 --- a/config.json +++ b/config.json @@ -8,5 +8,6 @@ "USER": "pyshelf", "PASSWORD": "pyshelf", "BOOKSHELF": "data/shelf.json", - "ALLOWED_HOSTS": "*" + "ALLOWED_HOSTS": "*", + "SECRET": "r0m#@$d(cs^si9_jmm)z-z#6-4-(snoctd)l(4becso9k=dwvs" } \ No newline at end of file diff --git a/docker/development.docker-compose.yml b/docker/development.docker-compose.yml new file mode 100644 index 0000000..8ece1da --- /dev/null +++ b/docker/development.docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.7" + +# This file is used to test the docker image. To host pyShelf yourself for +# production, please use the official pyShelf image on +# https://hub.docker.com/r/pyshelf/pyshelf + +# For development, use the following command in the root folder: +# `docker-compose -f .\docker\development.docker-compose.yml up --build` + +services: + db: + image: "postgres" + environment: + - "POSTGRES_PASSWORD=pyshelf" + - "POSTGRES_USER=pyshelf" + - "POSTGRES_DB=pyshelf" + volumes: + - "db_data:/var/lib/postgresql/data/" + + pyshelf: + build: + context: .. + dockerfile: ./docker/Dockerfile + ports: + - "8080:8000" + volumes: + - "${LOCAL_BOOK_DIR}:/books" + depends_on: + - db + +volumes: + db_data: \ No newline at end of file diff --git a/doxygen.conf b/doxygen.conf index b33a684..44466aa 100755 --- a/doxygen.conf +++ b/doxygen.conf @@ -38,13 +38,13 @@ PROJECT_NAME = "pyShelf Open Source Ebook Server" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 0.4.1 +PROJECT_NUMBER = 0.6.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. -PROJECT_BRIEF = "Open source, console based E-book server" +PROJECT_BRIEF = "FOSS E-Book Server, https://pyshelf.com" # With the PROJECT_LOGO tag one can specify a logo or an icon that is included # in the documentation. The maximum height of the logo should not exceed 55 diff --git a/importBooks b/importBooks index 272b643..c3ad6e8 100755 --- a/importBooks +++ b/importBooks @@ -5,9 +5,10 @@ import sys from src.backend.lib.storage import Storage from src.backend.pyShelf_ScanLibrary import execute_scan - +from src.backend.pyShelf_MakeCollections import MakeCollections PRG_PATH = pathlib.Path.cwd() LIB_PATH = pathlib.Path.joinpath(PRG_PATH, "src", "backend", "lib") sys.path.insert(0, PRG_PATH) print("\n") execute_scan(PRG_PATH) +MakeCollections(PRG_PATH) diff --git a/src/backend/lib/config.py b/src/backend/lib/config.py index c36c0c3..155bebd 100755 --- a/src/backend/lib/config.py +++ b/src/backend/lib/config.py @@ -36,6 +36,7 @@ class Config: self.allowed_hosts = _data["ALLOWED_HOSTS"] self.db_user = _data["USER"] self.db_pass = _data["PASSWORD"] + self.SECRET = _data["SECRET"] def open_file(self, _cp): """ diff --git a/src/frontend/settings.py b/src/frontend/settings.py index 5542394..21ccfa7 100755 --- a/src/frontend/settings.py +++ b/src/frontend/settings.py @@ -30,8 +30,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "@(9b9jslgg41u1u=mr)-2*-n2x0vef0zsy39*z@sz18&tvow18" - +# SECRET_KEY = "@(9b9jslgg41u1u=mr)-2*-n2x0vef0zsy39*z@sz18&tvow18" +SECRET_KEY = CONFIG.SECRET # SECURITY WARNING: don't run with debug turned on in production! DEBUG = TEMPLATE_DEBUG = False if DEBUG is True: diff --git a/src/frontend/urls.py b/src/frontend/urls.py index 2d8f4c6..532dc8e 100755 --- a/src/frontend/urls.py +++ b/src/frontend/urls.py @@ -15,17 +15,17 @@ Including another URLconf """ from django.conf import settings from django.contrib import admin -from django.urls import include, path, re_path from django.contrib.auth import views as auth_views from django.contrib.auth.models import User from django.shortcuts import HttpResponse +from django.urls import include, path, re_path from interface import views urlpatterns = [ path("admin/", admin.site.urls), path("", views.index, name="index"), path("home", views.home, name="home"), - re_path("^live", views.live, name="liverequest"), + re_path("^live$", views.live, name="live"), path("sort/<_order>", views.index, name="index"), path("flip_sort/<_order>", views.flip_sort, name="index"), path("download/", views.download, name="download"), diff --git a/src/interface/admin.py b/src/interface/admin.py index 96c61be..56212ba 100755 --- a/src/interface/admin.py +++ b/src/interface/admin.py @@ -22,7 +22,6 @@ class CustomUserAdmin(UserAdmin): ) -# Register your models here. admin.site.register(Books) admin.site.register(Collections) admin.site.register(Favorites) diff --git a/src/interface/static/css/main.css b/src/interface/static/css/main.css index a946d94..0e2d542 100644 --- a/src/interface/static/css/main.css +++ b/src/interface/static/css/main.css @@ -10754,4 +10754,8 @@ a.nav_link { .center { width: fit-content !important; margin: auto; +} + +.collection { + cursor: pointer; } \ No newline at end of file diff --git a/src/interface/static/css/main.scss b/src/interface/static/css/main.scss index 065059a..d13398c 100755 --- a/src/interface/static/css/main.scss +++ b/src/interface/static/css/main.scss @@ -618,3 +618,6 @@ a.nav_link { width: fit-content !important; margin: auto; } +.collection{ + cursor: pointer; +} diff --git a/src/interface/static/js/pyshelf_ux.js b/src/interface/static/js/pyshelf_ux.js index f23e013..008cf3b 100755 --- a/src/interface/static/js/pyshelf_ux.js +++ b/src/interface/static/js/pyshelf_ux.js @@ -18,7 +18,7 @@ $(document).ready(function(){ var max_height = win_height - (hdr_height + ftr_height) - (scr_height - win_height); // Set our available height var u_string = "Username"; var p_string = "Password"; - var s_string = "search by Title, Author, Tags, or Collections" + var s_string = "search by Title, Author, Tags, or Collections"; customlog([cmp_height]); $(".search_submit").click(function(){ var query = $('.nav_search').val(); @@ -35,8 +35,8 @@ $(document).ready(function(){ }); $('.nav_link').on('mouseover', function (e){ var popover_str = $(this).attr('alt'); - x = $(this).offset().left - y = $(this).offset().top + x = $(this).offset().left; + y = $(this).offset().to; $('.popover').html(popover_str); $('.popover').css('left', x+nav_width); $('.popover').css('top', y); @@ -44,15 +44,15 @@ $(document).ready(function(){ }); $('.nav_link').on('mouseout', function (e){ var popover_str = $(this).attr('alt'); - x = $(this).offset().left - y = $(this).offset().top + x = $(this).offset().left; + y = $(this).offset().top; $('.popover').html(popover_str); $('.popover').css('left', x); $('.popover').css('top', y); $('.popover').css('display','none'); }); $('#btn_collections').on('click', function (e){ - $('.hidden.vert-nav.collections').toggle() + $('.hidden.vert-nav.collections').toggle(); }); $('.input_box').on('click', function(){ $(this).attr("value",""); @@ -78,16 +78,16 @@ $(document).ready(function(){ var optionSelected = $(this).find("option:selected"); var valueSelected = optionSelected.val(); var textSelected = optionSelected.text(); - window.location.href="/"+valueSelected + window.location.href="/"+valueSelected; }); $('#btn-home').on("click", function(){ _location = $(this).attr('data-location'); window.location.href=_location; }); $('#flip_sort').on("click", function(){ - window.location.href="/flip_sort/"+$("#_order").val() + window.location.href="/flip_sort/"+$("#_order").val(); }); - $('#search_string').html(" "+$('#_search').val().substr(0,15)+"") + $('#search_string').html(" "+$('#_search').val().substr(0,15)+""); $('#pop_over_0').dialog({ autoOpen: false }); resize_search(); @@ -97,10 +97,10 @@ $(document).ready(function(){ window.location.href = '/show_collection/'+$(this).attr('data'); }); $('#btn_login').on('click', function() { - var isopen = $('#pop_over_0').dialog("isOpen") + var isopen = $('#pop_over_0').dialog("isOpen"); if (isopen) { $('#pop_over_0').dialog("close"); - return + return; } customlog(['Login Clicked']); $.ajax({ @@ -138,10 +138,10 @@ $(document).ready(function(){ window.location.href = '/logout'; }); $('#coll_button').on('click', function(){ - var isopen = $('#pop_over_0').dialog("isOpen") + var isopen = $('#pop_over_0').dialog("isOpen"); if (isopen){ $('#pop_over_0').dialog("close"); - return + return; } customlog(['Collections Clicked']); $.ajax({ @@ -171,7 +171,49 @@ $(document).ready(function(){ error: function(response){ customlog(["Failure", response]); } - }) + }); + }); + $('.info-button').on('click', function(){ + var isopen = $('#pop_over_0').dialog("isOpen"); + if (isopen){ + $('#pop_over_0').dialog("close"); + return; + } + customlog(['Book Details Clicked', $(this).attr('data')]); + $.ajax({ + type: "GET", url: "/live", data: {hook: 'details', pk:$(this).attr('data')}, + success: function(response){ + // Set the dialog title + $('#pop_over_0').dialog({ + title: response.data['title'], + maxHeight: (win_height-100), + minWidth: $("#horiz_nav_main").width(), + hide: { effect: "blind", duration: 1000 }, + show: { effect: "blind", duration: 1000 }, + position: { my: "center center", at: "center center", of: window + } + }); + // clear and create a new container + $('#pop_over_0').html('
'); + // Populate the container from response.data + $('#book_expanded').append('
Title
'+response.data['title']+'
') + $('#book_expanded').append('
Author
'+response.data['author']+'
') + if (response.data['description']!== null){ + $('#book_expanded').append('
Expanded Description
'+response.data['description']+'
') + + } + if (response.data['tags'] !== null){ + $('#book_expanded').append('
Tags
'+response.data['tags']+'
') + } + // Close the container + $('#pop_over').append('
'); + // Now open this dialog + $('#pop_over_0').dialog("open"); + }, + error: function(response){ + customlog(["Failure", response]); + } + }); }); }); function resize_search(win_width){ diff --git a/src/interface/templates/index.html b/src/interface/templates/index.html index 60bafe3..731e7ab 100755 --- a/src/interface/templates/index.html +++ b/src/interface/templates/index.html @@ -120,8 +120,8 @@ - - + + diff --git a/src/interface/views.py b/src/interface/views.py index 449bc58..030c70d 100755 --- a/src/interface/views.py +++ b/src/interface/views.py @@ -1,22 +1,22 @@ +import json import os from base64 import b64decode, b64encode from pathlib import Path from backend.lib.config import Config +from django.conf import settings +from django.contrib import auth +from django.contrib.auth import authenticate, get_user_model, login, logout +from django.contrib.auth.models import User from django.db import models from django.http import JsonResponse -from django.shortcuts import HttpResponse, render, redirect # render_to_response -from django.utils.text import slugify -from django.contrib.auth import login, authenticate, logout -from django.contrib import auth -from django.contrib.auth.models import User -from django.conf import settings -from django.contrib.auth import get_user_model -from django.utils.datastructures import MultiValueDictKeyError -import json -from .forms import SignUpForm, UserLoginForm -from .models import Books, Collections, Navigation, Favorites, User +from django.shortcuts import HttpResponse, redirect, render # render_to_response from django.template.loader import render_to_string +from django.utils.datastructures import MultiValueDictKeyError +from django.utils.text import slugify + +from .forms import SignUpForm, UserLoginForm +from .models import Books, Collections, Favorites, Navigation, User config = Config(Path("../")) @@ -380,6 +380,12 @@ def collections_list(): def live(request, **kwargs): + """ + Respond to live requests. Primarily used as a response object for Ajax calls + :param GET['hook']: collection_listing, book_details, register + :param kwargs['pk']: Primary key of requested object + :return: JsonResponse Object, status response code + """ err_txt = {"err": "There is no responder for your request"} try: hook = request.GET['hook'] except MultiValueDictKeyError as e: return JsonResponse(err_txt, status=404) @@ -387,8 +393,11 @@ def live(request, **kwargs): if hook == "collection_listing": collections = collections_list() return JsonResponse({"data": collections}, status=200) - elif hook == "book_details": - return JsonResponse({"data": Books.objects.get(pk=kwargs['pk'])}, status=200) + elif hook == "details": + try: _pk = request.GET['pk'] + except KeyError as e: return False + book = book_details(Books.objects.get(pk=_pk)) + return JsonResponse({"data": book}, status=200) elif hook == "register": html = render_to_string('signup.html', {'form': SignUpForm}, request) html += render_to_string('login.html', {'form': UserLoginForm}, request) @@ -397,7 +406,15 @@ def live(request, **kwargs): return JsonResponse({"data": "Response sent"}, status=200) - +def book_details(book): + return { + 'title': book.title, + 'author': book.author, + 'description': book.description, + 'tags': book.tags, + 'rights': book.rights, + 'pk': book.id + } def payload(request, query, _set, _limit, _order, **kwargs): """ Return formatted data to template @@ -445,10 +462,10 @@ def payload(request, query, _set, _limit, _order, **kwargs): _r, _r_len = \ _results.order_by(_order)[_set_min:_set_max],\ _results.count() - else: + else: # No new query was passed try: query = request.session['cached_query'] # Is there a cached query? - if query == None: raise KeyError + if query == None: raise KeyError # No cached query exists jump to KeyError if request.session['ascending']: _results = Books().generic_search(query) else: _results = Books().generic_search(query).reverse() @@ -464,7 +481,7 @@ def payload(request, query, _set, _limit, _order, **kwargs): _bookstats = Books.objects.all().count() if (_r_len): _btotal = str(_r_len) else: _btotal = str(_bookstats) - + # Format the payload and return it to the view return { "Books": _r, "Set": str(_set), @@ -479,4 +496,3 @@ def payload(request, query, _set, _limit, _order, **kwargs): "SearchLen": _r_len, "Order": _order, } -