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/)
|
||||||
@@ -90,7 +126,7 @@ Running via the Django test server might be possible, albeit not recomended.
|
|||||||
#### 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": "", "DB_HOST": "localhost", "DB_PORT": "5432", "DATABASE": "pyshelf", "USER": "pyshelf", "PASSWORD": "pyshelf", "BOOKSHELF": "data/shelf.json", "ALLOWED_HOSTS": "*", "hostname": "localhost", "webport": "8000", "wsgiport": "8001"}
|
||||||
0
importBooks.pstat
vendored
Normal file
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
requirements.txt
vendored
@@ -18,3 +18,5 @@ prompt_toolkit
|
|||||||
psutil
|
psutil
|
||||||
pyfiglet
|
pyfiglet
|
||||||
mobi-python
|
mobi-python
|
||||||
|
pudb
|
||||||
|
jsonpickle
|
||||||
@@ -26,9 +26,10 @@ 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.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.books = None
|
self.books = None
|
||||||
self.db_pointer = config.catalogue_db
|
self.db_pointer = config.catalogue_db
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -70,6 +71,7 @@ class Catalogue:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def process_by_filetype(self, book):
|
def process_by_filetype(self, book):
|
||||||
|
print(str(book), end='\r', flush=True)
|
||||||
if book.endswith(".epub"):
|
if book.endswith(".epub"):
|
||||||
epub = self.process_epub(book)
|
epub = self.process_epub(book)
|
||||||
return self.extract_metadata_epub(epub)
|
return self.extract_metadata_epub(epub)
|
||||||
@@ -108,6 +110,11 @@ class Catalogue:
|
|||||||
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]
|
||||||
@@ -116,11 +123,57 @@ class Catalogue:
|
|||||||
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
|
@staticmethod
|
||||||
def extract_metadata_mobi(book):
|
def stripTags(source):
|
||||||
|
p = re.compile(r"<.*?>")
|
||||||
|
return p.sub("", source)
|
||||||
|
|
||||||
|
def extract_metadata_mobi(self, book):
|
||||||
book = Mobi(book)
|
book = Mobi(book)
|
||||||
book.parse()
|
book.parse()
|
||||||
try:
|
try:
|
||||||
@@ -129,9 +182,43 @@ class Catalogue:
|
|||||||
cover_image = None
|
cover_image = None
|
||||||
title = book.title().decode("utf-8")
|
title = book.title().decode("utf-8")
|
||||||
author = book.author().decode("utf-8")
|
author = book.author().decode("utf-8")
|
||||||
breakpoint()
|
book_config = book.config
|
||||||
# TODO some files are still passing encoded data for author.
|
try:
|
||||||
return [title, author, cover_image, book.f.name]
|
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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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, rights, tags) values (%s, %s, %s, 0, %s, 0, %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
|
||||||
|
# book[7], # date # TODO: set import time
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ DATABASES = {
|
|||||||
"PASSWORD": CONFIG.password,
|
"PASSWORD": CONFIG.password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -21,17 +21,20 @@ 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="index"),
|
||||||
|
path("sort/<_order>", views.index, name="index"),
|
||||||
path("download/<pk>", views.download, name="download"),
|
path("download/<pk>", views.download, name="download"),
|
||||||
|
path("favorite/<pk>", views.favorite, name="favorite"),
|
||||||
|
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(
|
path("search/<query>", views.index, name="search"),
|
||||||
"show_collection/<_collection>/<_colset>",
|
path("search/<query>/<_set>", views.index, name="search"),
|
||||||
views.show_collection,
|
path("show_collection/<_collection>/<_colset>", views.show_collection, name="show_collection",),
|
||||||
name="show_collection",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|||||||
35
src/interface/migrations/0005_navigation.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 3.0.4 on 2020-06-10 05:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("interface", "0004_collections"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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",},
|
||||||
|
),
|
||||||
|
]
|
||||||
69
src/interface/migrations/0006_auto_20200617_2333.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2020-06-17 23:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interface', '0005_navigation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Users',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uname', models.CharField(max_length=255)),
|
||||||
|
('fname', models.CharField(max_length=255, null=True)),
|
||||||
|
('lname', models.CharField(max_length=255, null=True)),
|
||||||
|
('email', models.CharField(max_length=255, null=True)),
|
||||||
|
('password', models.CharField(max_length=255, null=True)),
|
||||||
|
('ulvl', models.IntegerField(null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='books',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='books',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='books',
|
||||||
|
name='identifier',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='books',
|
||||||
|
name='publisher',
|
||||||
|
field=models.CharField(max_length=266, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='books',
|
||||||
|
name='rights',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='books',
|
||||||
|
name='tags',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Favorites',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('favorite', models.ManyToManyField(to='interface.Books')),
|
||||||
|
('uname', models.ManyToManyField(to='interface.Users')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'favorites',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -29,17 +29,22 @@ class Books(models.Model):
|
|||||||
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.CharField(max_length=255, null=False)
|
||||||
|
description = models.TextField(null=True)
|
||||||
|
identifier = models.CharField(max_length=255, null=True)
|
||||||
|
publisher = models.CharField(max_length=266, null=True)
|
||||||
|
date = models.DateField(null=True)
|
||||||
|
rights = models.CharField(max_length=255, null=True)
|
||||||
|
tags = models.CharField(max_length=255, 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
|
||||||
|
|
||||||
|
|
||||||
class Collections(models.Model):
|
class Collections(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "collections"
|
db_table = "collections"
|
||||||
@@ -62,3 +67,100 @@ class Collections(models.Model):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Users(models.Model):
|
||||||
|
"""
|
||||||
|
pyShelfs User Database class
|
||||||
|
:param uname: User Name
|
||||||
|
:param fname: First Name
|
||||||
|
:param lname: Last Name
|
||||||
|
:param email: User Email Address
|
||||||
|
:param password: User Password
|
||||||
|
:param ulvl: User Level
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "users"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
uname = models.CharField(max_length=255)
|
||||||
|
fname = models.CharField(max_length=255, null=True)
|
||||||
|
lname = models.CharField(max_length=255, null=True)
|
||||||
|
email = models.CharField(max_length=255, null=True, editable=True)
|
||||||
|
password = models.CharField(max_length=255, null=True)
|
||||||
|
ulvl = models.IntegerField(null=True)
|
||||||
|
|
||||||
|
def generic_search(self, query):
|
||||||
|
try:
|
||||||
|
results = Users.objects.annotate(
|
||||||
|
search=SearchVector("uname", "email", "lname"),
|
||||||
|
).filter(search=query)
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class Favorites(models.Model):
|
||||||
|
"""
|
||||||
|
pyShelfs User Database class
|
||||||
|
:param uname: User Name
|
||||||
|
:param fname: First Name
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "favorites"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
favorite = models.ManyToManyField(Books)
|
||||||
|
uname = models.ManyToManyField(Users)
|
||||||
|
|
||||||
|
def generic_search(self, query):
|
||||||
|
try:
|
||||||
|
results = Favorites.objects.annotate(search=SearchVector("uname"),).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 |