Merge branch 'newui' into 0.5.0--docker
1
.gitignore
vendored
@@ -2,6 +2,7 @@ books/*
|
|||||||
*.epub
|
*.epub
|
||||||
*.idea
|
*.idea
|
||||||
*.pyc
|
*.pyc
|
||||||
|
.svn/
|
||||||
app/content.opf
|
app/content.opf
|
||||||
.vscode
|
.vscode
|
||||||
fontend/db.sqlite3
|
fontend/db.sqlite3
|
||||||
|
|||||||
78
README.md
vendored
@@ -1,16 +1,18 @@
|
|||||||
# pyShelf 0.5.0
|
# pyShelf 0.6.0
|
||||||
|
|
||||||
<p align="center"><b>Terminal based ebook server. Open source & Lightweight.</b></p>
|
<p align="center"><b>Terminal based ebook server. Open source & Lightweight.</b></p>
|
||||||
<p align="center">Having used Calibre for hosting my eBook collection in the past, I found myself frustrated having to install X on my server, or manage my library externally, Thus I have decided to spin up my own.</p>
|
<p align="center">Having used Calibre for hosting my eBook collection in the past, I found myself frustrated having to install X on my server, or manage my library externally, Thus I have decided to spin up my own.</p>
|
||||||
<p align="center"><a href="https://pyshelf.com">https://pyshelf.com</a></p>
|
<p align="center"><a href="https://pyshelf.com">https://pyshelf.com</a></p>
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||
|
|
||||||
<p align="center"><b>Discord [https://discord.gg/H9TbNJS](https://discord.gg/H9TbNJS) | IRC freenode.net @ #pyshelf</b></p>
|
|
||||||
|
|
||||||
|
### You dont need a X server to host a website, or your Movie & Tv collection, so why should you need one to host ebooks?
|
||||||
|
<i>Other solutiions require you to have access to an X server to at the very least generate your book database, pyShelf doesnt.We aim to provide a fully featured ebook server with minimal requirements, and no reliance on X whatsoever.</i>
|
||||||
|
|
||||||
|
Follow or influence development @ <p align="center"><b> <a href="https://discord.gg/H9TbNJS">Discord</a> | <a href="https://webchat.freenode.net/#pyshelf">IRC</a> freenode.net @ #pyshelf</b></p>
|
||||||
## Current Features
|
## Current Features
|
||||||
* Custom Installer -- pre-req installs work on Arch Based Distros Only
|
* Custom Installer works only on Arch Based Distros
|
||||||
* Recursive Scanning
|
* Recursive Scanning
|
||||||
* Fast database access
|
* Fast database access
|
||||||
* Django based frontend
|
* Django based frontend
|
||||||
@@ -20,36 +22,64 @@
|
|||||||
|
|
||||||
## Currently Supported Formats
|
## Currently Supported Formats
|
||||||
* epub
|
* epub
|
||||||
|
* mobi
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0 Patch Notes.
|
||||||
|
# New Features
|
||||||
|
* .mobi Yep mobis are now a thing!
|
||||||
|
* Result set ordering
|
||||||
|
* You can now choose to order your results:
|
||||||
|
* Title
|
||||||
|
* Author
|
||||||
|
* Categories
|
||||||
|
* & Tags
|
||||||
|
* Reworked UI/UX
|
||||||
|
* More intuitive, less intrusive, & stays out of the way. <i>caveat: I need to rework the placement of the next & previous page controls. While they do remain usable, I intend to have them follow the users</i>
|
||||||
|
position on the page in future releases.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* New controls
|
||||||
|
* Sort
|
||||||
|
* Ascending / Descending result set
|
||||||
|
* Display of the result set count, and your current position in the set.
|
||||||
|
* A pop over layer to hold things like
|
||||||
|
* [ ] User login
|
||||||
|
* [ ] Control panel
|
||||||
|
* [ ] Book details
|
||||||
|
* Whatever else :)
|
||||||
## Installation Example
|
## Installation Example
|
||||||
<a href="https://vimeo.com/382292764" target="_blank">pyShelf Installation Video</a>
|
<a href="https://vimeo.com/382292764" target="_blank">pyShelf Installation Video</a>
|
||||||
|
|
||||||
## Further Installation & Support Information
|
## Further Installation & Support Information
|
||||||
* [SUPPORT.md](https://github.com/th3r00t/pyShelf/blob/development/.github/SUPPORT.md)
|
* [SUPPORT.md](https://github.com/th3r00t/pyShelf/blob/development/.github/SUPPORT.md)
|
||||||
|
|
||||||
## 0.5.0 Patch Notes.
|
|
||||||
|
|
||||||
### Pre-req Dependencies
|
### Pre-req Dependencies
|
||||||
* gcc -- This will be installed by the new pre-installer script if its binary is not detected at /usr/bin/gcc
|
* gcc -- This will be installed by the new pre-installer script if its binary is not detected at /usr/bin/gcc
|
||||||
Users on distros other then Arch should install gcc via their systems package manager prior to
|
Users on distros other then Arch should install gcc via their systems package manager prior to
|
||||||
running the installer.
|
running the installer.
|
||||||
* Python3
|
* Python3
|
||||||
* pip
|
* pip
|
||||||
### New Features
|
|
||||||
* Collections
|
|
||||||
We are now categorizing your ebooks into collections based on the folder
|
|
||||||
structure used to store them. Any folder after the root book folder is now
|
|
||||||
considered as a collection.
|
|
||||||
#### books/forgotten realms/ -> Forgotten Realms Collection.
|
|
||||||
#### books/Dune/Prelude To Dune -> Dune, & Preluse To Dune Collections.
|
|
||||||
|
|
||||||
In addition to the work on the collection system, a good deal of time was spent
|
# Installation
|
||||||
on the installer, and the concept of having an installer in the first place.
|
This project is currently targeted towards Network Administrators, and other home
|
||||||
|
|
||||||
I mainly wanted to make this project for Network Administrators, and other home
|
|
||||||
enthusiasts whom I assume will know how to setup a Django app, and a
|
enthusiasts whom I assume will know how to setup a Django app, and a
|
||||||
Postgres server. Beyond that theres nothing the user has to do to make the
|
Postgres server.
|
||||||
system work...
|
|
||||||
|
Once your environment is ready very little is required to get the system up and running
|
||||||
|
* From the main directory
|
||||||
|
* setup configurations as discussed in [SUPPORT.md](https://github.com/th3r00t/pyShelf/blob/development/.github/SUPPORT.md)
|
||||||
|
* `pip install -r requirments.txt`
|
||||||
|
* `cd src`
|
||||||
|
* `python manage.py migrate`
|
||||||
|
* `cd ..`
|
||||||
|
* `./importbooks`
|
||||||
|
* `./makecollections`
|
||||||
|
* Browse to the site as defined in your apache | nginx config
|
||||||
|
|
||||||
|
## Included installer
|
||||||
|
<a href="https://vimeo.com/382292764" target="_blank">pyShelf Installation Video</a>
|
||||||
|
|
||||||
The installer will only run correctly on arch based distros. This could be
|
The installer will only run correctly on arch based distros. This could be
|
||||||
easily rectified to include other package managers, Members of the community
|
easily rectified to include other package managers, Members of the community
|
||||||
@@ -61,6 +91,12 @@ installation already present in the source now, however it is not complete and
|
|||||||
should not be relied upon to be present in future releases unless completed by
|
should not be relied upon to be present in future releases unless completed by
|
||||||
a member of the community,
|
a member of the community,
|
||||||
|
|
||||||
|
The installer will walk you through all the configurations required by pyShelf to
|
||||||
|
run if you are running on Arch linux.
|
||||||
|
|
||||||
|
## Further Installation & Support Information
|
||||||
|
* [SUPPORT.md](https://github.com/th3r00t/pyShelf/blob/development/.github/SUPPORT.md)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
* [`pre-commit`](https://pre-commit.com/)
|
* [`pre-commit`](https://pre-commit.com/)
|
||||||
@@ -118,7 +154,7 @@ Once the .epub files are in the directory specified in [docker/.env](docker/.env
|
|||||||
#### Improved cover image storage, and acquisition.
|
#### Improved cover image storage, and acquisition.
|
||||||
#### OPDS Support
|
#### OPDS Support
|
||||||
#### Support for other formats
|
#### Support for other formats
|
||||||
- [ ] .mobi
|
- [x] .mobi
|
||||||
- [ ] .pdf
|
- [ ] .pdf
|
||||||
- [ ] .cbz
|
- [ ] .cbz
|
||||||
- [ ] .zip (Zipped book folders, is this a new idea? (Consider storing your library folders zipped and retrieving a book on demand))
|
- [ ] .zip (Zipped book folders, is this a new idea? (Consider storing your library folders zipped and retrieving a book on demand))
|
||||||
|
|||||||
2
config.json
vendored
@@ -1 +1 @@
|
|||||||
{"TITLE": "pyShelf E-Book Server", "VERSION": "0.5.0", "BOOKPATH": "/srv/Books", "DB_HOST": "localhost", "DB_PORT": "5432", "DATABASE": "pyshelf", "USER": "pyshelf", "PASSWORD": "pyshelf", "BOOKSHELF": "data/shelf.json", "ALLOWED_HOSTS": "*", "hostname": "localhost", "webport": "8000", "wsgiport": "8001"}
|
{"TITLE": "pyShelf E-Book Server", "VERSION": "0.6.0", "BOOKPATH": "/home/raelon/Books", "DB_HOST": "localhost", "DB_PORT": "5432", "DATABASE": "pyshelf", "USER": "pyshelf", "PASSWORD": "pyshelf", "BOOKSHELF": "data/shelf.json", "ALLOWED_HOSTS": "*", "hostname": "localhost", "webport": "8000", "wsgiport": "8001"}
|
||||||
2
importBooks
vendored
@@ -1,4 +1,4 @@
|
|||||||
#!python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
0
importBooks.pstat
vendored
Normal file
11
installer
vendored
@@ -1,4 +1,4 @@
|
|||||||
#!python
|
#!/usr/bin/ env python
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -304,8 +304,8 @@ if RequiredServices().db_server_found(req) is False:
|
|||||||
if r["name"] == "PASSWORD":
|
if r["name"] == "PASSWORD":
|
||||||
sql_pass = r["answer"]
|
sql_pass = r["answer"]
|
||||||
|
|
||||||
# sql_user = config["USER"]
|
sql_user = config["USER"]
|
||||||
sql_user = "pyshelf"
|
# sql_user = "pyshelf"
|
||||||
db_name = "pyshelf"
|
db_name = "pyshelf"
|
||||||
psql_cmd = (
|
psql_cmd = (
|
||||||
"CREATE DATABASE %s; CREATE USER %s WITH PASSWORD '%s'; \
|
"CREATE DATABASE %s; CREATE USER %s WITH PASSWORD '%s'; \
|
||||||
@@ -333,6 +333,11 @@ if RequiredServices().db_server_found(req) is False:
|
|||||||
psql_cmd,
|
psql_cmd,
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
for r in install_answers:
|
||||||
|
if r["name"] == "PASSWORD":
|
||||||
|
sql_pass = r["answer"]
|
||||||
|
sql_user = config["USER"]
|
||||||
|
db_name = "pyshelf"
|
||||||
psql_cmd = (
|
psql_cmd = (
|
||||||
"CREATE DATABASE %s; CREATE USER %s WITH PASSWORD '%s'; \
|
"CREATE DATABASE %s; CREATE USER %s WITH PASSWORD '%s'; \
|
||||||
GRANT ALL PRIVILEGES ON DATABASE %s TO %s;"
|
GRANT ALL PRIVILEGES ON DATABASE %s TO %s;"
|
||||||
|
|||||||
2
makeCollections
vendored
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
BIN
preview_050.png
vendored
|
Before Width: | Height: | Size: 1.7 MiB |
BIN
preview_1_050.png
vendored
|
Before Width: | Height: | Size: 2.4 MiB |
2
pyproject.toml
vendored
@@ -7,4 +7,4 @@ use_parentheses = true
|
|||||||
# NOTE: the known_third_party setting is managed by
|
# NOTE: the known_third_party setting is managed by
|
||||||
# seed-isort-config and should not be modified directly.
|
# seed-isort-config and should not be modified directly.
|
||||||
# Any changes made to this setting will be overwritten.
|
# Any changes made to this setting will be overwritten.
|
||||||
known_third_party = ["backend", "bs4", "django", "interface", "prompt_toolkit", "psycopg2", "pyfiglet", "requests"]
|
known_third_party = ["backend", "bs4", "django", "interface", "mobi", "prompt_toolkit", "psycopg2", "pyfiglet", "requests"]
|
||||||
|
|||||||
4
requirements.txt
vendored
@@ -17,3 +17,7 @@ psycopg2-binary
|
|||||||
prompt_toolkit
|
prompt_toolkit
|
||||||
psutil
|
psutil
|
||||||
pyfiglet
|
pyfiglet
|
||||||
|
mobi-python
|
||||||
|
pudb
|
||||||
|
jsonpickle
|
||||||
|
django-widget-tweaks
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -7,6 +7,8 @@ import zipfile
|
|||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from mobi import Mobi
|
||||||
|
|
||||||
from .api_hooks import DuckDuckGo
|
from .api_hooks import DuckDuckGo
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .storage import Storage
|
from .storage import Storage
|
||||||
@@ -24,10 +26,11 @@ class Catalogue:
|
|||||||
self.opf_regx = re.compile(r"\.opf")
|
self.opf_regx = re.compile(r"\.opf")
|
||||||
self.cover_regx = re.compile(r"\.jpg|\.jpeg|\.png|\.bmp|\.gif")
|
self.cover_regx = re.compile(r"\.jpg|\.jpeg|\.png|\.bmp|\.gif")
|
||||||
self.html_regx = re.compile(r"\.html")
|
self.html_regx = re.compile(r"\.html")
|
||||||
|
self.title_sanitization_regx = re.compile(r"^(Book )+[0-9]*")
|
||||||
|
self.title_sanitization_lvl2_regx = re.compile(r"^(Book )+[0-9]*\W+(-)")
|
||||||
|
self.title_sanitization_dirs_regx = re.compile(r"/")
|
||||||
self.root_dir = config.root
|
self.root_dir = config.root
|
||||||
self.book_folder = config.book_path
|
self.book_folder = config.book_path
|
||||||
self.book_shelf = config.book_shelf
|
|
||||||
self._book_list_expanded = None
|
|
||||||
self.books = None
|
self.books = None
|
||||||
self.db_pointer = config.catalogue_db
|
self.db_pointer = config.catalogue_db
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -57,23 +60,30 @@ class Catalogue:
|
|||||||
:returns self._book_list_expanded: json string containing all book metadata
|
:returns self._book_list_expanded: json string containing all book metadata
|
||||||
"""
|
"""
|
||||||
self.scan_folder() # Populate file list
|
self.scan_folder() # Populate file list
|
||||||
regx = re.compile(r"\.epub")
|
regx = re.compile(r"\.epub|\.mobi")
|
||||||
try:
|
try:
|
||||||
self.books = list(filter(regx.search, filter(None, self.file_list)))
|
self.books = list(filter(regx.search, filter(None, self.file_list)))
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
print(e)
|
print(e)
|
||||||
self._book_list_expanded = {}
|
"""
|
||||||
with open(self.book_shelf, "w") as f:
|
|
||||||
for book in self.books:
|
for book in self.books:
|
||||||
self._book_list_expanded[book] = self.process_book(book)
|
self._book_list_expanded[book] = self.process_by_filetype(book)
|
||||||
json.dump(self._book_list_expanded, f)
|
|
||||||
return self._book_list_expanded
|
return self._book_list_expanded
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_by_filetype(self, book):
|
||||||
|
print(str(book), end='\r', flush=True)
|
||||||
|
if book.endswith(".epub"):
|
||||||
|
epub = self.process_epub(book)
|
||||||
|
return self.extract_metadata_epub(epub)
|
||||||
|
elif book.endswith(".mobi"):
|
||||||
|
return self.extract_metadata_mobi(book)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_book(book):
|
def process_epub(book):
|
||||||
"""Return dictionary of epub file contents"""
|
"""Return dictionary of epub file contents"""
|
||||||
book = zipfile.ZipFile(book, "r")
|
|
||||||
details = {}
|
details = {}
|
||||||
|
book = zipfile.ZipFile(book, "r")
|
||||||
with book as book_zip:
|
with book as book_zip:
|
||||||
details["files"] = []
|
details["files"] = []
|
||||||
details["path"] = book.filename
|
details["path"] = book.filename
|
||||||
@@ -86,7 +96,7 @@ class Catalogue:
|
|||||||
details["files"].append(match.string)
|
details["files"].append(match.string)
|
||||||
return details
|
return details
|
||||||
|
|
||||||
def extract_metadata(self, book):
|
def extract_metadata_epub(self, book):
|
||||||
"""
|
"""
|
||||||
Return extracted metadata and cover picture
|
Return extracted metadata and cover picture
|
||||||
book['path'] == Full path to ebook file
|
book['path'] == Full path to ebook file
|
||||||
@@ -94,24 +104,123 @@ class Catalogue:
|
|||||||
"""
|
"""
|
||||||
book_zip = zipfile.ZipFile(book["path"], "r")
|
book_zip = zipfile.ZipFile(book["path"], "r")
|
||||||
with book_zip as f:
|
with book_zip as f:
|
||||||
content = self.extract_content(book_zip, book)
|
content = self.extract_content(f, book)
|
||||||
soup = BeautifulSoup(content, "lxml")
|
soup = BeautifulSoup(content, "lxml")
|
||||||
title = soup.find("dc:title")
|
title = soup.find("dc:title")
|
||||||
if title is None:
|
if title is None:
|
||||||
title = book["path"].split("/")[-1].rsplit(".", 1)[0]
|
title = book["path"].split("/")[-1].rsplit(".", 1)[0]
|
||||||
else:
|
else:
|
||||||
title = title.contents[0]
|
title = title.contents[0]
|
||||||
|
if re.match(self.title_sanitization_regx, title):
|
||||||
|
if re.match(self.title_sanitization_lvl2_regx, title):
|
||||||
|
title = re.split(r"-+\W", title)[1]
|
||||||
|
else: title = re.split(self.title_sanitization_regx, title)[2]
|
||||||
|
|
||||||
author = soup.find("dc:creator")
|
author = soup.find("dc:creator")
|
||||||
if author is not None:
|
if author is not None:
|
||||||
author = author.contents[0]
|
author = author.contents[0]
|
||||||
try:
|
try:
|
||||||
cover = self.extract_cover_image(book_zip, book)
|
cover = self.extract_cover_image(f, book)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# cover = self.extract_cover_html(book_zip, book)
|
# cover = self.extract_cover_html(book_zip, book)
|
||||||
cover = DuckDuckGo().image_result(title)
|
cover = DuckDuckGo().image_result(title)
|
||||||
book_details = [title, author, cover, book["path"]]
|
try:
|
||||||
|
description = self.stripTags(soup.find("dc:description").text)
|
||||||
|
except AttributeError:
|
||||||
|
description = None
|
||||||
|
try:
|
||||||
|
identifier = self.stripTags(soup.find("dc:identifier").text)
|
||||||
|
except AttributeError:
|
||||||
|
identifier = None
|
||||||
|
try:
|
||||||
|
publisher = self.stripTags(soup.find("dc:publisher").text)
|
||||||
|
except AttributeError:
|
||||||
|
publisher = None
|
||||||
|
try:
|
||||||
|
date = self.stripTags(soup.find("dc:date").text)
|
||||||
|
except AttributeError:
|
||||||
|
date = None
|
||||||
|
try:
|
||||||
|
rights = self.stripTags(soup.find("dc:rights").text)
|
||||||
|
except AttributeError:
|
||||||
|
rights = None
|
||||||
|
try:
|
||||||
|
tags = soup.find_all("dc:subject")
|
||||||
|
except AttributeError:
|
||||||
|
tags = None
|
||||||
|
ftags = None
|
||||||
|
if tags is not None:
|
||||||
|
for tag in tags:
|
||||||
|
if ftags is None:
|
||||||
|
ftags = tag.text
|
||||||
|
else:
|
||||||
|
ftags = ftags + "," + tag.text
|
||||||
|
book_details = [
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
cover,
|
||||||
|
book["path"],
|
||||||
|
description,
|
||||||
|
identifier,
|
||||||
|
publisher,
|
||||||
|
date,
|
||||||
|
rights,
|
||||||
|
ftags,
|
||||||
|
]
|
||||||
return book_details
|
return book_details
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stripTags(source):
|
||||||
|
p = re.compile(r"<.*?>")
|
||||||
|
return p.sub("", source)
|
||||||
|
|
||||||
|
def extract_metadata_mobi(self, book):
|
||||||
|
book = Mobi(book)
|
||||||
|
book.parse()
|
||||||
|
try:
|
||||||
|
cover_image = book.readImageRecord(0)
|
||||||
|
except KeyError:
|
||||||
|
cover_image = None
|
||||||
|
title = book.title().decode("utf-8")
|
||||||
|
author = book.author().decode("utf-8")
|
||||||
|
book_config = book.config
|
||||||
|
try:
|
||||||
|
description = self.stripTags(book_config['exth']['records'][103].decode("utf-8"))
|
||||||
|
except KeyError:
|
||||||
|
description = None
|
||||||
|
try:
|
||||||
|
identifier = book_config['exth']['records'][104].decode("utf-8")
|
||||||
|
except KeyError:
|
||||||
|
identifier = None
|
||||||
|
try:
|
||||||
|
publisher = book_config['exth']['records'][101].decode("utf-8")
|
||||||
|
except KeyError:
|
||||||
|
publisher = None
|
||||||
|
date = None
|
||||||
|
rights = None
|
||||||
|
try:
|
||||||
|
ftags = book_config['exth']['records'][105].decode("utf-8")
|
||||||
|
if ":" in ftags:
|
||||||
|
ftags = ftags.replace(":", ",")
|
||||||
|
elif ";" in ftags:
|
||||||
|
ftags = ftags.replace(";", ",")
|
||||||
|
# elif re.search(r"\s", ftags): # Must be final assignment to avoid spliting on multiple delimeters
|
||||||
|
# ftags = ftags.replace(" ", ",")
|
||||||
|
except KeyError:
|
||||||
|
ftags = None
|
||||||
|
return [
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
cover_image,
|
||||||
|
book.f.name,
|
||||||
|
description,
|
||||||
|
identifier,
|
||||||
|
publisher,
|
||||||
|
date,
|
||||||
|
rights,
|
||||||
|
ftags,
|
||||||
|
]
|
||||||
|
|
||||||
def extract_content(self, book_zip, book):
|
def extract_content(self, book_zip, book):
|
||||||
"""
|
"""
|
||||||
Opens epub as zip file filters then stores as list any files matching opf_regx
|
Opens epub as zip file filters then stores as list any files matching opf_regx
|
||||||
@@ -161,12 +270,13 @@ class Catalogue:
|
|||||||
Gets a list of new files via compare_shelf_current.
|
Gets a list of new files via compare_shelf_current.
|
||||||
Iterates over list and inserts new books into database.
|
Iterates over list and inserts new books into database.
|
||||||
"""
|
"""
|
||||||
|
# TODO Refactor metadata extraction into process_book \
|
||||||
|
# call to more easily handle additional formats
|
||||||
book_list = self.compare_shelf_current()
|
book_list = self.compare_shelf_current()
|
||||||
db = Storage(self.config)
|
db = Storage(self.config)
|
||||||
for book in book_list:
|
for book in book_list:
|
||||||
book = self.process_book(book)
|
book = self.process_by_filetype(book)
|
||||||
extracted = self.extract_metadata(book)
|
db.insert_book(book)
|
||||||
db.insert_book(extracted)
|
|
||||||
inserted = db.commit()
|
inserted = db.commit()
|
||||||
if inserted is not True:
|
if inserted is not True:
|
||||||
print(inserted)
|
print(inserted)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
import re
|
import re
|
||||||
|
import datetime
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class Storage:
|
|||||||
Insert book in database
|
Insert book in database
|
||||||
:returns: True if succeeds False if not
|
:returns: True if succeeds False if not
|
||||||
"""
|
"""
|
||||||
q = "INSERT INTO books (title, author, cover, progress, file_name, pages) values (%s, %s, %s, 0, %s, 0);"
|
q = "INSERT INTO books (title, author, cover, progress, file_name, pages, description, identifier, publisher, date, rights, tags) values (%s, %s, %s, 0, %s, 0, %s, %s, %s, %s, %s, %s);"
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cover_image = book[2].data
|
cover_image = book[2].data
|
||||||
@@ -61,11 +61,27 @@ class Storage:
|
|||||||
cover_image = book[2]
|
cover_image = book[2]
|
||||||
if not book[2]: # If cover image is missing unset entry
|
if not book[2]: # If cover image is missing unset entry
|
||||||
cover_image = None
|
cover_image = None
|
||||||
self.cursor.execute(q, (book[0], book[1], cover_image, book[3]))
|
self.cursor.execute(
|
||||||
|
q,
|
||||||
|
(
|
||||||
|
book[0], # title
|
||||||
|
book[1], # author
|
||||||
|
cover_image,
|
||||||
|
book[3], # file
|
||||||
|
book[4], # descr
|
||||||
|
book[5], # ident
|
||||||
|
book[6], # publisher
|
||||||
|
datetime.datetime.now(),
|
||||||
|
book[8], # rights
|
||||||
|
book[9], # tags
|
||||||
|
),
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
if e.pgcode == '22007': # psycopg2's error code for invalid date
|
||||||
return False
|
book[7] = psycopg2.Date(int(book[7]), 1, 1)
|
||||||
|
self.insert_book(book)
|
||||||
|
raise e
|
||||||
|
|
||||||
def book_paths_list(self):
|
def book_paths_list(self):
|
||||||
"""
|
"""
|
||||||
@@ -106,8 +122,11 @@ class Storage:
|
|||||||
path = self.config.book_path + "/"
|
path = self.config.book_path + "/"
|
||||||
_collections = []
|
_collections = []
|
||||||
_pathing = book[1].split(path)[1].split("/")
|
_pathing = book[1].split(path)[1].split("/")
|
||||||
|
try:
|
||||||
_pathing.pop(0)
|
_pathing.pop(0)
|
||||||
_pathing.pop(-1)
|
_pathing.pop(-1)
|
||||||
|
except IndexError:
|
||||||
|
continue
|
||||||
for _p in _pathing:
|
for _p in _pathing:
|
||||||
_s = _p.replace("'", "")
|
_s = _p.replace("'", "")
|
||||||
_x = re.sub(_title_regx, "", _s)
|
_x = re.sub(_title_regx, "", _s)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ INSTALLED_APPS = [
|
|||||||
"interface",
|
"interface",
|
||||||
"interface.templatetags",
|
"interface.templatetags",
|
||||||
"debug_toolbar",
|
"debug_toolbar",
|
||||||
|
"widget_tweaks"
|
||||||
]
|
]
|
||||||
|
AUTH_USER_MODEL = "interface.User"
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
@@ -103,6 +104,10 @@ DATABASES = {
|
|||||||
"PORT": CONFIG.db_port,
|
"PORT": CONFIG.db_port,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# Session
|
||||||
|
# Uncomment below to enable sessions management by a memcache server
|
||||||
|
# https://docs.djangoproject.com/en/3.0/topics/http/sessions/
|
||||||
|
# SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||||
@@ -133,6 +138,6 @@ USE_TZ = True
|
|||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
|
LOGIN_REDIRECT_URL = 'home'
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, "interface/static/")
|
STATIC_ROOT = os.path.join(BASE_DIR, "interface/static/")
|
||||||
|
|||||||
@@ -16,21 +16,56 @@ Including another URLconf
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path, re_path
|
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 interface import views
|
from interface import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
|
path("home", views.home, name="home"),
|
||||||
|
re_path("^live", views.live, name="liverequest"),
|
||||||
|
path("sort/<_order>", views.index, name="index"),
|
||||||
|
path("flip_sort/<_order>", views.flip_sort, name="index"),
|
||||||
path("download/<pk>", views.download, name="download"),
|
path("download/<pk>", views.download, name="download"),
|
||||||
|
path("share/<pk>", views.share, name="share"),
|
||||||
|
path("share/<pk>", views.info, name="info"),
|
||||||
path("prev_page/<bookset>", views.prev_page, name="prev_page"),
|
path("prev_page/<bookset>", views.prev_page, name="prev_page"),
|
||||||
path("next_page/<bookset>", views.next_page, name="next_page"),
|
path("next_page/<bookset>", views.next_page, name="next_page"),
|
||||||
path("search/", views.search, name="search"),
|
path("prev_page/<bookset>/<_order>", views.prev_page, name="prev_page"),
|
||||||
path("search/<query>", views.search, name="search"),
|
path("next_page/<bookset>/<_order>", views.next_page, name="next_page"),
|
||||||
path("search/<query>/<_set>", views.search, name="search"),
|
path("search/", views.index, name="search"),
|
||||||
|
path("search/<query>", views.index, name="search"),
|
||||||
|
path("search/<query>/<_set>", views.index, name="search"),
|
||||||
|
path("collections", views.collectionspage, name="collections"),
|
||||||
|
path("show_collection/<query>/<_set>", views.show_collection, name="show_collection"),
|
||||||
|
path("signup", views.signup, name="signup"),
|
||||||
|
path("login", views.userlogin, name="login"),
|
||||||
|
path('logout', views.userlogout, name='logout'),
|
||||||
|
path('favorite/<pk>', views.favorite, name='favorite'),
|
||||||
|
path('favorites', views.favorites, name='favorites'),
|
||||||
|
path('favorites/<bookset>', views.favorites, name='favorites'),
|
||||||
|
path('favorites/<bookset>/<query>', views.favorites, name='favorites'),
|
||||||
path(
|
path(
|
||||||
"show_collection/<_collection>/<_colset>",
|
'admin/password_reset/',
|
||||||
views.show_collection,
|
auth_views.PasswordResetView.as_view(),
|
||||||
name="show_collection",
|
name='admin_password_reset',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'admin/password_reset/done/',
|
||||||
|
auth_views.PasswordResetDoneView.as_view(),
|
||||||
|
name='password_reset_done',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'reset/<uidb64>/<token>/',
|
||||||
|
auth_views.PasswordResetConfirmView.as_view(),
|
||||||
|
name='password_reset_confirm',
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'reset/done/',
|
||||||
|
auth_views.PasswordResetCompleteView.as_view(),
|
||||||
|
name='password_reset_complete',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from .models import Books, Collections, Favorites, Navigation, User
|
||||||
|
from .forms import CustomUserCreationForm, CustomUserChangeForm
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
model = User
|
||||||
|
add_form = CustomUserCreationForm
|
||||||
|
form = CustomUserChangeForm
|
||||||
|
list_display = ["email", "username", "facebook", "twitter", "sponsorid", "matrixid"]
|
||||||
|
fieldsets = UserAdmin.fieldsets + (
|
||||||
|
(None, {"fields": ()}),
|
||||||
|
("Personal info", {"fields": ("facebook", "twitter", "matrixid")}),
|
||||||
|
("Permissions", {"fields": ("sponsorid",)}),
|
||||||
|
)
|
||||||
|
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
|
||||||
|
# overrides get_fieldsets to use this attribute when creating a user.
|
||||||
|
add_fieldsets = UserAdmin.add_fieldsets + (
|
||||||
|
(None, {"classes": ("wide",), "fields": ("facebook", "twitter", "sponsorid", "matrixid")},),
|
||||||
|
)
|
||||||
|
|
||||||
from .models import Books
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
admin.site.register(Books)
|
admin.site.register(Books)
|
||||||
|
admin.site.register(Collections)
|
||||||
|
admin.site.register(Favorites)
|
||||||
|
admin.site.register(Navigation)
|
||||||
|
admin.site.register(User, CustomUserAdmin)
|
||||||
|
|||||||
64
src/interface/forms.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserCreationForm(UserCreationForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("username", "email", "facebook", "twitter", "sponsorid", "matrixid")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserChangeForm(UserChangeForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("username", "email", "facebook", "twitter", "sponsorid", "matrixid")
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserLoginForm(AuthenticationForm):
|
||||||
|
class Meta:
|
||||||
|
Model = User
|
||||||
|
fields = ("username", "password")
|
||||||
|
|
||||||
|
|
||||||
|
class SignUpForm(CustomUserCreationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CustomUserCreationForm, self).__init__(*args, **kwargs)
|
||||||
|
for fieldname in ['password1']:
|
||||||
|
self.fields[fieldname].help_text = 'At least 8 chars.'
|
||||||
|
self.fields[fieldname].initial = 'Password'
|
||||||
|
for fieldname in ['password2']:
|
||||||
|
self.fields[fieldname].help_text = ''
|
||||||
|
self.fields[fieldname].initial = 'Confirm Pass'
|
||||||
|
|
||||||
|
username = forms.CharField(
|
||||||
|
max_length=30,
|
||||||
|
required=True,
|
||||||
|
help_text='Required',
|
||||||
|
initial="",
|
||||||
|
label="Username",
|
||||||
|
|
||||||
|
)
|
||||||
|
email = forms.EmailField(
|
||||||
|
max_length=254,
|
||||||
|
help_text='Required',
|
||||||
|
initial="",
|
||||||
|
label="Email"
|
||||||
|
)
|
||||||
|
matrixid = forms.CharField(
|
||||||
|
max_length=30,
|
||||||
|
required=False,
|
||||||
|
help_text='Optional',
|
||||||
|
initial="",
|
||||||
|
label="Matrix Id"
|
||||||
|
)
|
||||||
|
password1 = forms.PasswordInput()
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("username", "email", "matrixid")
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoginForm(CustomUserLoginForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
116
src/interface/migrations/0001_initial.py
Executable file → Normal file
@@ -1,35 +1,111 @@
|
|||||||
# Generated by Django 2.2.7 on 2019-11-28 19:24
|
# Generated by Django 3.0.7 on 2020-07-23 16:01
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
('auth', '0011_update_proxy_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Books",
|
name='User',
|
||||||
fields=[
|
fields=[
|
||||||
(
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
"id",
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
models.AutoField(
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
auto_created=True,
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
primary_key=True,
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
serialize=False,
|
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
|
||||||
verbose_name="ID",
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
),
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
),
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
("title", models.CharField(max_length=255)),
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
("author", models.CharField(max_length=255, null=True)),
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
("categories", models.CharField(max_length=255, null=True)),
|
('facebook', models.CharField(max_length=255, null=True)),
|
||||||
("cover", models.BinaryField(editable=True, null=True)),
|
('twitter', models.CharField(max_length=255, null=True)),
|
||||||
("pages", models.IntegerField(null=True)),
|
('ulvl', models.IntegerField(default=1)),
|
||||||
("progress", models.IntegerField(null=True)),
|
('sponsorid', models.IntegerField(null=True)),
|
||||||
("file_name", models.CharField(max_length=255)),
|
('matrixid', models.CharField(max_length=255, null=True)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
],
|
],
|
||||||
options={"db_table": "books",},
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Books',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.TextField()),
|
||||||
|
('author', models.CharField(max_length=255, null=True)),
|
||||||
|
('categories', models.TextField(null=True)),
|
||||||
|
('cover', models.BinaryField(editable=True, null=True)),
|
||||||
|
('pages', models.IntegerField(null=True)),
|
||||||
|
('progress', models.IntegerField(null=True)),
|
||||||
|
('file_name', models.TextField()),
|
||||||
|
('description', models.TextField(null=True)),
|
||||||
|
('identifier', models.CharField(max_length=255, null=True)),
|
||||||
|
('publisher', models.TextField(null=True)),
|
||||||
|
('date', models.DateField(null=True)),
|
||||||
|
('rights', models.CharField(max_length=255, null=True)),
|
||||||
|
('tags', models.TextField(null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'books',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Navigation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('link', models.CharField(max_length=255, null=True)),
|
||||||
|
('category', models.CharField(max_length=255, null=True)),
|
||||||
|
('parent_id', models.IntegerField(null=True)),
|
||||||
|
('alt', models.CharField(max_length=255, null=True)),
|
||||||
|
('type', models.IntegerField(null=True)),
|
||||||
|
('socket', models.CharField(max_length=255)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'navigation',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Favorites',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('book', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='interface.Books')),
|
||||||
|
('user', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'favorites',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Collections',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('collection', models.CharField(max_length=255)),
|
||||||
|
('book_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='interface.Books')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'collections',
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# Generated by Django 2.2.7 on 2020-01-01 04:45
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("interface", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(name="books", options={"managed": False},),
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Generated by Django 2.2.7 on 2020-01-01 04:47
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("interface", "0002_auto_20200101_0445"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(name="books", options={},),
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Generated by Django 3.0.2 on 2020-02-04 20:22
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("interface", "0003_auto_20200101_0447"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Collections",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("collection", models.CharField(max_length=255)),
|
|
||||||
(
|
|
||||||
"book_id",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to="interface.Books",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={"db_table": "collections",},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
0
src/interface/migrations/__init__.py
Executable file → Normal file
@@ -1,5 +1,8 @@
|
|||||||
from django.contrib.postgres.search import SearchVector
|
from django.contrib.postgres.search import SearchVector
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import AbstractUser, User
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
@@ -22,30 +25,43 @@ class Books(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.TextField(max_length=None)
|
||||||
author = models.CharField(max_length=255, null=True)
|
author = models.CharField(max_length=255, null=True)
|
||||||
categories = models.CharField(max_length=255, null=True)
|
categories = models.TextField(max_length=None, null=True)
|
||||||
cover = models.BinaryField(null=True, editable=True)
|
cover = models.BinaryField(null=True, editable=True)
|
||||||
pages = models.IntegerField(null=True)
|
pages = models.IntegerField(null=True)
|
||||||
progress = models.IntegerField(null=True)
|
progress = models.IntegerField(null=True)
|
||||||
file_name = models.CharField(max_length=255, null=False)
|
file_name = models.TextField(max_length=None, null=False)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
description = models.TextField(null=True)
|
||||||
|
identifier = models.CharField(max_length=255, null=True)
|
||||||
|
publisher = models.TextField(max_length=None, null=True)
|
||||||
|
date = models.DateField(null=True)
|
||||||
|
rights = models.CharField(max_length=255, null=True)
|
||||||
|
tags = models.TextField(max_length=None, null=True)
|
||||||
|
|
||||||
def generic_search(self, query):
|
def generic_search(self, query):
|
||||||
try:
|
try:
|
||||||
results = Books.objects.annotate(
|
results = Books.objects.annotate(
|
||||||
search=SearchVector("title", "file_name", "author"),
|
search=SearchVector("title", "file_name", "author", "tags"),
|
||||||
).filter(search=query)
|
).filter(search=query)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def search_by_collection(self, query):
|
||||||
|
try:
|
||||||
|
return Books.objects.filter(categories=query)
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class Collections(models.Model):
|
class Collections(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "collections"
|
db_table = "collections"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.collection
|
return self.collection.__str__()
|
||||||
|
|
||||||
collection = models.CharField(max_length=255)
|
collection = models.CharField(max_length=255)
|
||||||
book_id = models.ForeignKey(Books, on_delete=models.PROTECT)
|
book_id = models.ForeignKey(Books, on_delete=models.PROTECT)
|
||||||
@@ -55,10 +71,109 @@ class Collections(models.Model):
|
|||||||
return reverse("model-detail-view", args=[str(self.id)])
|
return reverse("model-detail-view", args=[str(self.id)])
|
||||||
|
|
||||||
def generic_search(self, query):
|
def generic_search(self, query):
|
||||||
|
books =[]
|
||||||
try:
|
try:
|
||||||
results = Books.objects.annotate(search=SearchVector("collection"),).filter(
|
#results = Collections.objects.all().filter(
|
||||||
|
# collection=query
|
||||||
|
#)
|
||||||
|
results = Collections.objects.prefetch_related('book_id').annotate(search=SearchVector("collection"), ).filter(
|
||||||
search=query
|
search=query
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
|
_results = results.values('book_id')
|
||||||
|
for r in results:
|
||||||
|
books.append(
|
||||||
|
{
|
||||||
|
"pk": r.book_id.id,
|
||||||
|
"title": r.book_id.title,
|
||||||
|
"author": r.book_id.author,
|
||||||
|
"categories": r.book_id.categories,
|
||||||
|
"cover": r.book_id.cover,
|
||||||
|
"pages": r.book_id.pages,
|
||||||
|
"progress": r.book_id.progress,
|
||||||
|
"file_name": r.book_id.file_name,
|
||||||
|
"date": r.book_id.date,
|
||||||
|
"description": r.book_id.description,
|
||||||
|
"identifier": r.book_id.identifier,
|
||||||
|
"publisher": r.book_id.publisher,
|
||||||
|
"date": r.book_id.date,
|
||||||
|
"rights": r.book_id.rights,
|
||||||
|
"tags": r.book_id.tags
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return books
|
||||||
|
|
||||||
|
|
||||||
|
class Navigation(models.Model):
|
||||||
|
"""
|
||||||
|
pyShelfs Navigation Database class
|
||||||
|
:param title: Link Text
|
||||||
|
:param link: Link link :)
|
||||||
|
:param category: Where in the nav tree do I belong
|
||||||
|
:param parent_id: This link is a sub link of link with id of me
|
||||||
|
:param alt: Alternate text of link
|
||||||
|
:param type: Web link, or Socket link which will be expected to act on \
|
||||||
|
the link, and the action defined in socket
|
||||||
|
:param socket: if a Socket link define socket here
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "navigation"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
link = models.CharField(max_length=255, null=True)
|
||||||
|
category = models.CharField(max_length=255, null=True)
|
||||||
|
parent_id = models.IntegerField(null=True, editable=True)
|
||||||
|
alt = models.CharField(max_length=255, null=True)
|
||||||
|
type = models.IntegerField(null=True)
|
||||||
|
socket = models.CharField(max_length=255, null=False)
|
||||||
|
|
||||||
|
def generic_search(self, query):
|
||||||
|
try:
|
||||||
|
results = Navigation.objects.annotate(
|
||||||
|
search=SearchVector("title", "parent_id", "category"),
|
||||||
|
).filter(search=query)
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
facebook = models.CharField(max_length=255, null=True)
|
||||||
|
twitter = models.CharField(max_length=255, null=True)
|
||||||
|
ulvl = models.IntegerField(default=1)
|
||||||
|
sponsorid = models.IntegerField(null=True)
|
||||||
|
matrixid = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
|
class Favorites(models.Model):
|
||||||
|
"""
|
||||||
|
Favorites Database class
|
||||||
|
:param book: book foreign key
|
||||||
|
:param user: user foreign key
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "favorites"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.book)
|
||||||
|
|
||||||
|
book = models.ForeignKey(Books, on_delete=models.CASCADE, default=None)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, default=None)
|
||||||
|
|
||||||
|
def generic_search(self, query):
|
||||||
|
try:
|
||||||
|
results = Favorites.objects.annotate(search=SearchVector("user"),).filter(search=query)
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
src/interface/static/admin/css/autocomplete.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/base.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/changelists.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/dashboard.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/fonts.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/forms.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/login.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/responsive.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/responsive_rtl.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/rtl.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/vendor/select2/LICENSE-SELECT2.md
vendored
Executable file → Normal file
0
src/interface/static/admin/css/vendor/select2/select2.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/vendor/select2/select2.min.css
vendored
Executable file → Normal file
0
src/interface/static/admin/css/widgets.css
vendored
Executable file → Normal file
0
src/interface/static/admin/fonts/LICENSE.txt
vendored
Executable file → Normal file
0
src/interface/static/admin/fonts/README.txt
vendored
Executable file → Normal file
0
src/interface/static/admin/fonts/Roboto-Bold-webfont.woff
vendored
Executable file → Normal file
0
src/interface/static/admin/fonts/Roboto-Light-webfont.woff
vendored
Executable file → Normal file
0
src/interface/static/admin/fonts/Roboto-Regular-webfont.woff
vendored
Executable file → Normal file
0
src/interface/static/admin/img/LICENSE
vendored
Executable file → Normal file
0
src/interface/static/admin/img/README.txt
vendored
Executable file → Normal file
0
src/interface/static/admin/img/calendar-icons.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
src/interface/static/admin/img/gis/move_vertex_off.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
src/interface/static/admin/img/gis/move_vertex_on.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
src/interface/static/admin/img/icon-addlink.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 331 B |
0
src/interface/static/admin/img/icon-alert.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
0
src/interface/static/admin/img/icon-calendar.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
src/interface/static/admin/img/icon-changelink.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 380 B |
0
src/interface/static/admin/img/icon-clock.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 677 B After Width: | Height: | Size: 677 B |
0
src/interface/static/admin/img/icon-deletelink.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 392 B |
0
src/interface/static/admin/img/icon-no.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 560 B |
0
src/interface/static/admin/img/icon-unknown-alt.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 655 B |
0
src/interface/static/admin/img/icon-unknown.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 655 B |
0
src/interface/static/admin/img/icon-viewlink.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 581 B After Width: | Height: | Size: 581 B |
0
src/interface/static/admin/img/icon-yes.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 436 B After Width: | Height: | Size: 436 B |
0
src/interface/static/admin/img/inline-delete.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 560 B |
0
src/interface/static/admin/img/search.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 458 B After Width: | Height: | Size: 458 B |
0
src/interface/static/admin/img/selector-icons.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
0
src/interface/static/admin/img/sorting-icons.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
0
src/interface/static/admin/img/tooltag-add.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 331 B |
0
src/interface/static/admin/img/tooltag-arrowright.svg
vendored
Executable file → Normal file
|
Before Width: | Height: | Size: 280 B After Width: | Height: | Size: 280 B |