Files
books/main.py

757 lines
39 KiB
Python

from flask import Flask, render_template, request, redirect, jsonify
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import SQLAlchemyError
from flask_marshmallow import Marshmallow
from flask_bootstrap import Bootstrap
from wtforms import SubmitField, StringField, HiddenField, SelectField, IntegerField, TextAreaField, validators
from flask_wtf import FlaskForm
from status import st, Status
import re
import socket
####################################### Flask App globals #######################################
DEV_HOST="mara"
hostname = socket.gethostname()
print( "Running on: {}".format( hostname) )
app = Flask(__name__)
### what is this value? I gather I should chagne it?
# local DB conn string
if hostname == DEV_HOST:
DB_URL = 'postgresql+psycopg2://ddp:NWNlfa01@127.0.0.1:5432/library'
else:
DB_URL = 'postgresql+psycopg2://ddp:blahdeblah@bookdb:5432/library'
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
db = SQLAlchemy(app)
ma = Marshmallow(app)
Bootstrap(app)
################################# Now, import non-book classes ###################################
from author import Author, AuthorForm, AuthorSchema, GetAuthors
from publisher import Publisher, PublisherForm, PublisherSchema, GetPublisherById
from genre import Genre, GenreForm, GenreSchema, GetGenres
from condition import Condition, ConditionForm, ConditionSchema, GetConditionById
from covertype import Covertype, CovertypeForm, CovertypeSchema, GetCovertypeById
from owned import Owned, OwnedForm, OwnedSchema, GetOwnedById
from rating import Rating, RatingForm, RatingSchema, GetRatingById
from loan import Loan, LoanForm, LoanSchema
from series import Series, SeriesForm, SeriesSchema, ListOfSeriesWithMissingBooks, CalcAvgRating
####################################### CLASSES / DB model #######################################
class QuickParentBook:
parent=[]
def __repr__(self):
return "<parent: {}, publisher: {}, owned: {}, covertype: {}, condition: {}, blurb: {}>".format(self.parent, self.publisher, self.owned, self.covertype, self.condition, self.blurb )
book_author_link = db.Table('book_author_link', db.Model.metadata,
db.Column('book_id', db.Integer, db.ForeignKey('book.id')),
db.Column('author_id', db.Integer, db.ForeignKey('author.id'))
)
book_genre_link = db.Table('book_genre_link', db.Model.metadata,
db.Column('book_id', db.Integer, db.ForeignKey('book.id')),
db.Column('genre_id', db.Integer, db.ForeignKey('genre.id'))
)
class Book_Loan_Link(db.Model):
__tablename__ = "book_loan_link"
book_id = db.Column(db.Integer, db.ForeignKey('book.id'), unique=True, nullable=False, primary_key=True)
loan_id = db.Column(db.Integer, db.ForeignKey('loan.id'), unique=True, nullable=False, primary_key=True)
def __repr__(self):
return "<book_id: {}, loan_id: {}>".format(self.book_id, self.loan_id)
class Book_Series_Link(db.Model):
__tablename__ = "book_series_link"
book_id = db.Column(db.Integer, db.ForeignKey('book.id'), unique=True, nullable=False, primary_key=True)
series_id = db.Column(db.Integer, db.ForeignKey('series.id'), unique=True, nullable=False, primary_key=True)
book_num = db.Column(db.Integer)
def __repr__(self):
return "<book_id: {}, series_id: {}, book_num: {}>".format(self.book_id, self.series_id, self.book_num)
def SeriesBookNum(series_id, book_id):
bsl=Book_Series_Link.query.filter( Book_Series_Link.book_id==book_id, Book_Series_Link.series_id==series_id ).all()
return bsl[0].book_num
class Book_Sub_Book_Link(db.Model):
__tablename__ = "book_sub_book_link"
book_id = db.Column(db.Integer, db.ForeignKey('book.id'), unique=True, nullable=False, primary_key=True)
sub_book_id = db.Column(db.Integer, db.ForeignKey('book.id'), unique=True, nullable=False, primary_key=True)
sub_book_num = db.Column(db.Integer)
def __repr__(self):
return "<book_id: {}, sub_book_id: {}, sub_book_num: {}>".format(self.book_id, self.sub_book_id, self.sub_book_num)
class Book(db.Model):
id = db.Column(db.Integer, db.Sequence('book_id_seq'), primary_key=True )
title = db.Column(db.String(100), unique=True, nullable=False)
author = db.relationship('Author', secondary=book_author_link)
publisher = db.Column(db.Integer, db.ForeignKey('publisher.id'))
genre = db.relationship('Genre', secondary=book_genre_link )
loan = db.relationship('Loan', secondary=Book_Loan_Link.__table__);
series = db.relationship('Series', secondary=Book_Series_Link.__table__);
bsl = db.relationship('Book_Series_Link' )
year_published = db.Column(db.Integer)
condition = db.Column(db.Integer, db.ForeignKey('condition.id'))
covertype = db.Column(db.Integer, db.ForeignKey('covertype.id'))
owned = db.Column(db.Integer, db.ForeignKey('owned.id'))
rating = db.Column(db.Integer, db.ForeignKey('rating.id'))
notes = db.Column(db.Text)
blurb = db.Column(db.Text)
created = db.Column(db.DateTime)
modified = db.Column(db.DateTime)
# take actual parent book as there is no real associated sub_book_num data and can just use it
parent = db.relationship('Book', secondary=Book_Sub_Book_Link.__table__, primaryjoin="Book.id==Book_Sub_Book_Link.sub_book_id", secondaryjoin="Book.id==Book_Sub_Book_Link.book_id" )
# but use child_ref as sub_book_num is per book, and I can't connect an empty array of sub_book_nums to a child book array in "child"
child_ref = db.relationship('Book_Sub_Book_Link', secondary=Book_Sub_Book_Link.__table__, primaryjoin="Book.id==Book_Sub_Book_Link.book_id", secondaryjoin="Book.id==Book_Sub_Book_Link.sub_book_id", order_by="Book_Sub_Book_Link.sub_book_num" )
def IsParent(self):
if len(self.child_ref):
return True
else:
return False
def IsChild(self):
if len(self.parent):
return True
else:
return False
def FirstSubBookNum(self):
# need to work out the first sub book and return an id?
if self.IsParent():
return self.child_ref[0].sub_book_num
else:
return 1
def LastSubBookNum(self):
# need to work out the last sub book and return an id?
if self.IsParent():
# -1 subscript returns the last one
return self.child_ref[-1].sub_book_num
else:
return 1
def NumSubBooks(self):
if self.IsParent():
return len(self.child_ref)
else:
return 1
def MoveBookInSeries( self, series_id, amt ):
# if parent book, move all sub books instead
try:
if self.IsParent():
tmp_book=book_schema.dump(self)
for book in tmp_book['child_ref']:
tmp_bid=GetBookIdFromBookSubBookLinkByIdAndSubBookNum( self.id, book['sub_book_num'] )
bsl=Book_Series_Link.query.filter( Book_Series_Link.book_id==tmp_bid, Book_Series_Link.series_id==series_id ).all()
bsl[0].book_num=bsl[0].book_num+amt
else:
bsl=Book_Series_Link.query.filter( Book_Series_Link.book_id==self.id, Book_Series_Link.series_id==series_id ).all()
bsl[0].book_num=bsl[0].book_num+amt
db.session.commit()
return
except SQLAlchemyError as e:
st.SetAlert( "danger" )
st.SetMessage( e.orig )
return redirect( '/series/{}'.format(series_id) )
def __repr__(self):
return "<id: {}, author: {}, title: {}, year_published: {}, rating: {}, condition: {}, owned: {}, covertype: {}, notes: {}, blurb: {}, created: {}, modified: {}, publisher: {}, genre: {}, parent: {}>".format(self.id, self.author, self.title, self.year_published, self.rating, self.condition, self.owned, self.covertype, self.notes, self.blurb, self.created, self.modified, self.publisher, self.genre, self.parent )
class Book_Sub_Book_LinkSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = Book_Sub_Book_Link
class Book_Series_LinkSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = Book_Series_Link
# Note not ordering this in the code below as, I can't work out how to use
# jinja2 to iterate over orderedDict - seems it doesnt support it?
# so I just hacked a list of keys in book.html
class BookSchema(ma.SQLAlchemyAutoSchema):
author = ma.Nested(AuthorSchema, many=True)
genre = ma.Nested(GenreSchema, many=True)
loan = ma.Nested(LoanSchema, many=True)
series = ma.Nested(SeriesSchema, many=True)
child_ref = ma.Nested(Book_Sub_Book_LinkSchema, many=True)
class Meta: model = Book
class BookForm(FlaskForm):
id = HiddenField()
title = StringField('Title:', [validators.DataRequired()])
# author built by hand
# genre built by hand
publisher = SelectField( 'publisher', choices=[(c.id, c.name) for c in Publisher.query.order_by('name')] )
owned = SelectField( 'owned', choices=[(c.id, c.name) for c in Owned.query.order_by('id')] )
covertype = SelectField( 'covertype', choices=[(c.id, c.name) for c in Covertype.query.order_by('id')] )
condition = SelectField( 'condition', choices=[(c.id, c.name) for c in Condition.query.order_by('id')] )
year_published = IntegerField('Year Published:', validators=[validators.DataRequired(), validators.NumberRange(min=1900, max=2100)] )
rating = SelectField( 'rating', choices=[(c.id, c.name) for c in Rating.query.order_by('id')] )
notes = TextAreaField('Notes:')
blurb = TextAreaField('Blurb:')
submit = SubmitField('Save' )
delete = SubmitField('Delete' )
add_sub = SubmitField('Add Sub-Book' )
rem_sub = SubmitField('Remove Sub-Book from Parent' )
################################# helper functions ###################################
def GetBookIdFromSeriesByBookNum( series_id, book_num ):
tmp_book = Book_Series_Link.query.filter(Book_Series_Link.series_id==series_id,Book_Series_Link.book_num==book_num).all()
if len(tmp_book):
return tmp_book[0].book_id
else:
return None
def GetBookNumBySeriesAndBookId( series_id, book_id ):
tmp_book = Book_Series_Link.query.filter(Book_Series_Link.series_id==series_id,Book_Series_Link.book_id==book_id).all()
return tmp_book[0].book_num
def GetBookIdFromBookSubBookLinkByIdAndSubBookNum( book_id, sub_book_num ):
tmp_bsbl = Book_Sub_Book_Link.query.filter(Book_Sub_Book_Link.book_id==book_id, Book_Sub_Book_Link.sub_book_num==sub_book_num ).all()
return tmp_bsbl[0].sub_book_id
# ignore ORM, the sub_book_num comes from the link, but does not have any attached data, so can't tell which book it connects to.
# Just select sub_book data and hand add it to the books object, and use it in jinja2 to indent/order the books/sub books
# This data is used to sort/indent subbooks
def AddSubs(books):
subs = db.engine.execute ( "select * from book_sub_book_link" )
for row in subs:
index = next((i for i, item in enumerate(books) if item.id == row.sub_book_id), -1)
if index == -1:
continue
books[index].parent_id = row.book_id
books[index].sub_book_num = row.sub_book_num
# HACK: Couldn't work out ORM to excluded sub_book self-ref, so using basic python
# loop to remove sub_books from list
def RemSubs(books):
subs = db.engine.execute ( "select * from book_sub_book_link" )
for row in subs:
for i, item in enumerate(books):
if item.id == row.sub_book_id:
books.remove(item)
####################################### GLOBALS #######################################
# allow jinja2 to call these python functions directly
app.jinja_env.globals['GetCovertypeById'] = GetCovertypeById
app.jinja_env.globals['GetOwnedById'] = GetOwnedById
app.jinja_env.globals['GetConditionById'] = GetConditionById
app.jinja_env.globals['GetPublisherById'] = GetPublisherById
app.jinja_env.globals['GetRatingById'] = GetRatingById
app.jinja_env.globals['SeriesBookNum'] = SeriesBookNum
app.jinja_env.globals['ClearStatus'] = st.ClearMessage
app.jinja_env.globals['ListOfSeriesWithMissingBooks'] = ListOfSeriesWithMissingBooks
book_schema = BookSchema()
books_schema = BookSchema(many=True)
####################################### ROUTES #######################################
@app.route("/search", methods=["POST"])
def search():
if 'InDBox' in request.form:
# removes already loaned books from list of books to loan out
books = Book.query.outerjoin(Book_Loan_Link).filter(Book.title.ilike("%{}%".format(request.form['term'])),Book_Loan_Link.loan_id==None).all()
InDBox=1
else:
books = Book.query.filter(Book.title.ilike("%{}%".format(request.form['term']))).all()
InDBox=0
AddSubs(books)
return render_template("books.html", books=books, InDBox=InDBox)
@app.route("/books", methods=["GET"])
def books():
books = Book.query.all()
AddSubs(books)
return render_template("books.html", books=books )
@app.route("/books_for_loan/<id>", methods=["GET", "POST"])
def books_for_loan(id):
books = Book.query.join(Book_Loan_Link).filter(Book_Loan_Link.loan_id==id).order_by(Book.id).all()
if request.method == 'POST':
InDBox=1
else:
InDBox=0
return render_template("books_for_loan.html", books=books, loan_id=id, InDBox=InDBox)
def CalcMoveForBookInSeries( id, bid, dir ):
book1 = Book.query.get(bid)
# if parent book, then up needs first sub book, if down then need last sub book (as parent book does not have a book_num in a series, its sub books do)
if book1.IsParent():
if dir == "up":
tmp_bid = GetBookIdFromBookSubBookLinkByIdAndSubBookNum( book1.id, book1.FirstSubBookNum() )
else:
tmp_bid = GetBookIdFromBookSubBookLinkByIdAndSubBookNum( book1.id, book1.LastSubBookNum() )
moving_series_book_num = GetBookNumBySeriesAndBookId( id, tmp_bid )
else:
moving_series_book_num = GetBookNumBySeriesAndBookId( id, book1.id )
# moving_series_book_num is the book_num of the book in the series we are really moving (adjusted for parent/sub book if needed)
if dir == "up":
swapping_with_book_num = moving_series_book_num-1
else:
swapping_with_book_num = moving_series_book_num+1
# swapping_with_book_num is the book_num of the book in the series we are going to swap with (notionally next/prev book in series)
tmp_bid = GetBookIdFromSeriesByBookNum( id, swapping_with_book_num )
# okay, if tmp_bid is None, then there is no book we are swapping
# with (rare case of moving say book 7 of a series of 10, and we
# don't have details on book 6 or 8...
if tmp_bid == None:
if dir == "up":
book1.MoveBookInSeries( id, -1 )
else:
book1.MoveBookInSeries( id, 1 )
else:
book2 = Book.query.get( tmp_bid )
# if book2 is a sub book, then we need its parent as book2 instead, as we have to move the whole book as one
if book2.IsChild():
book2 = Book.query.get( book2.parent[0].id )
# By here: book1 is parent or normal book we are swapping with book2 which is parent or normal book
b1_numsb = book1.NumSubBooks()
b2_numsb = book2.NumSubBooks()
if dir == "up":
b1_move_by=-b2_numsb
b2_move_by=b1_numsb
else:
b1_move_by=b2_numsb
b2_move_by=-b1_numsb
# By here: b{1,2}_move_by is the sign & amount we are moving the relevant book (and its sub-books by). If it is a normal book,
# then its by +/-1, but if it is a parent book, then its +/- the num of sub_books as they all have to move too
# MoveBookInSeries -> moves the book and its sub_books
book1.MoveBookInSeries( id, b1_move_by )
book2.MoveBookInSeries( id, b2_move_by )
@app.route("/books_for_series/<id>", methods=["GET", "POST"])
def books_for_series(id):
if request.method == 'POST':
if 'move_button' in request.form:
# split form's pressed move_button to yield: dir ("up" or "down") and bid = book_id of book
dir, bid = request.form['move_button'].split('-')
book1 = Book.query.get(bid)
for bsl in book1.bsl:
CalcMoveForBookInSeries( bsl.series_id, bid, dir )
books = Book.query.join(Book_Series_Link).filter(Book_Series_Link.series_id==id).all()
series = Series.query.get(id)
return render_template("books_for_series.html", books=books, series=series)
@app.route("/subbooks_for_book/<id>", methods=["GET", "POST"])
def subbooks_for_book(id):
if request.method == 'POST':
if 'move_button' in request.form:
# split form's pressed move_button to yield: dir ("up" or "down") and bid1 = book_id of subbook
dir, bid1 = request.form['move_button'].split('-')
bsbl1 = Book_Sub_Book_Link.query.filter(Book_Sub_Book_Link.book_id==id, Book_Sub_Book_Link.sub_book_id==bid1 ).all()
if dir == "up":
swap_with_num = bsbl1[0].sub_book_num-1
else:
swap_with_num = bsbl1[0].sub_book_num+1
bid2=GetBookIdFromBookSubBookLinkByIdAndSubBookNum( id, swap_with_num )
bsbl2 = Book_Sub_Book_Link.query.filter(Book_Sub_Book_Link.book_id==id, Book_Sub_Book_Link.sub_book_id==bid2 ).all()
# swap the 2 books (by switching sub_book_nums)
tmp=bsbl1[0].sub_book_num
bsbl1[0].sub_book_num = bsbl2[0].sub_book_num
bsbl2[0].sub_book_num = tmp
db.session.commit()
####################################
# force sub books for jinja2 to be able to use (ORM is not giving all the details
####################################
subs = db.engine.execute ( "select bsb.book_id, bsb.sub_book_id, bsb.sub_book_num, book.title, \
r.name as rating, book.year_published, book.notes, \
bal.author_id as author_id, author.surname||', '||author.firstnames as author \
from book_sub_book_link bsb, book, book_author_link bal, author, rating r\
where bsb.book_id = {} and book.id = bsb.sub_book_id and book.id = bal.book_id and \
bal.author_id = author.id and r.id = book.rating \
order by bsb.sub_book_num".format( id ) )
sub_book=[]
for row in subs:
sub_book.append( { 'book_id': row.book_id, 'sub_book_id': row.sub_book_id, \
'sub_book_num': row.sub_book_num, 'title' : row.title, 'rating': row.rating,\
'year_published' : row.year_published, 'notes' : row.notes, 'author_id' : row.author_id, 'author' : row.author } )
return render_template("subbooks_for_book.html", sub_books=sub_book, s2=subs )
################################################################################
# /remove_sub_book -> POST -> removes this subbook from parent
# /books (or /book/<parent_id> -- if you added a sub-book of parent_id
################################################################################
@app.route("/remove_subbook", methods=["POST"])
def remove_sub_book():
try:
db.engine.execute("delete from book_sub_book_link where book_id = {} and sub_book_id = {}".format( request.form['rem_sub_parent_id'], request.form['rem_sub_sub_book_id']) )
db.session.commit()
return redirect( '/book/{}'.format(request.form['rem_sub_parent_id']) )
except SQLAlchemyError as e:
st.SetAlert( "danger" )
st.SetMessage( e.orig )
return redirect( '/book/{}'.format(request.form['rem_sub_sub_book_id']) )
################################################################################
# /book -> GET/POST -> creates a new book and when created, takes you back to
# /books (or /book/<parent_id> -- if you added a sub-book of parent_id
################################################################################
@app.route("/book", methods=["GET", "POST"])
def new_book():
form = BookForm(request.form)
page_title='Create new Book'
author_list = GetAuthors()
genre_list = GetGenres()
book_authors=[]
for el in request.form:
if 'author-' in el:
book_authors.append( Author.query.get( request.form[el] ) )
book_genres = []
for genre in genre_list:
if "genre-{}".format(genre.id) in request.form:
book_genres.append( genre )
if request.method == 'POST':
if 'add_sub' in request.form:
parent=request.form['add_sub_parent_id']
book = Book.query.get(parent)
bb=QuickParentBook()
bb.parent=[]
bb.parent.append( { 'id': parent, 'title': book.title } )
form.publisher.default = book.publisher
form.owned.default = book.owned
form.condition.default = book.condition
form.covertype.default = book.covertype
form.blurb.default = book.blurb
form.process()
return render_template("book.html", page_title='Create new (sub) Book', b=bb, books=None, book_form=form, author_list=author_list, genre_list=genre_list, alert="", message="", poss_series_list=ListOfSeriesWithMissingBooks() )
elif form.validate_on_submit() and len(book_genres):
book = Book( title=request.form['title'], owned=request.form['owned'], covertype=request.form['covertype'], condition=request.form['condition'], publisher=request.form['publisher'], year_published=request.form['year_published'], rating=request.form['rating'], notes=request.form['notes'], blurb=request.form['blurb'], genre=book_genres, author=book_authors )
db.session.add(book)
db.session.commit()
# this is a sub-book we have added
if 'parent_id' in request.form:
db.engine.execute( "insert into book_sub_book_link ( book_id, sub_book_id, sub_book_num ) values ( {}, {}, (select COALESCE(MAX(sub_book_num),0)+1 from book_sub_book_link where book_id = {}) )".format( request.form['parent_id'], book.id, request.form['parent_id'] ) )
parent=Book.query.get(request.form['parent_id'])
if len(parent.series) > 0:
# we have added a sub-book to something in a series
# already, so add a bsl for the next book_num
for s in parent.bsl:
db.engine.execute( "insert into book_series_link ( series_id, book_id, book_num ) values ( {}, {}, (select COALESCE(MAX(book_num),0)+1 from book_series_link where series_id={}) )".format( s.series_id, book.id, s.series_id ) )
db.session.commit()
st.SetMessage( "Created new Book ({})".format(book.title) )
cnt=1
for field in request.form:
if 'bsl-book_id-' in field and field != 'bsl-book_id-NUM':
cnt=int(re.findall( '\d+', field )[0])
sql="insert into book_series_link (book_id, series_id, book_num) values ( {}, {}, {} )".format( book.id, request.form['bsl-series_id-{}'.format(cnt)], request.form['bsl-book_num-{}'.format(cnt)])
db.engine.execute( sql )
cnt=cnt+1
return redirect( '/book/{}'.format(book.id) )
else:
alert="danger"
message="Failed to create Book"
for field in form.errors:
message = "{}<br>{}={}".format( message, field, form.errors[field] )
if len(book_genres) == 0:
message = "{}<br>genre=book has to have a genre selected".format( message )
print( "ERROR: Failed to create book: {}".format(message) )
book = Book( title=request.form["title"], owned=request.form['owned'], covertype=request.form['covertype'], condition=request.form['condition'], publisher=request.form['publisher'], year_published=request.form['year_published'], rating=request.form['rating'], notes=request.form['notes'], blurb=request.form['blurb'], genre=book_genres, author=book_authors )
if 'parent_id' in request.form:
bb=QuickParentBook()
bb.parent=[]
bb.parent.append( { 'id': request.form['parent_id'], 'title': request.form['parent_title'] } )
else:
bb=None
return render_template("book.html", page_title=page_title, b=bb, books=book, book_form=form, author_list=author_list, genre_list=genre_list, alert=alert, message=message, poss_series_list=ListOfSeriesWithMissingBooks() )
else:
return render_template("book.html", page_title=page_title, b=None, books=None, book_form=form, author_list=author_list, genre_list=genre_list, alert="success", message="", poss_series_list=ListOfSeriesWithMissingBooks() )
@app.route("/book/<id>", methods=["GET", "POST"])
def book(id):
book_form = BookForm(request.form)
page_title='Edit Book'
CheckSeriesChange=None
if request.method == 'POST':
if 'delete' in request.form:
book = Book.query.get(id)
if book.IsParent():
st.SetAlert( "danger" )
st.SetMessage( "This is a parent book, cannot delete it without deleting sub books first" )
return redirect( '/book/{}'.format(book.id) )
else:
st.SetAlert("success")
st.SetMessage("Deleted {}".format(book.title) )
pid = 0
if book.IsChild():
pid = book.parent[0].id
try:
db.session.delete(book)
db.session.commit()
except SQLAlchemyError as e:
st.SetAlert( "danger" )
st.SetMessage( e.orig )
return redirect( '/book/{}'.format(id) )
except Exception as e:
st.SetAlert( "danger" )
print("generic error")
if 'Dependency rule tried to blank-out primary key':
st.SetMessage( "<b>Failed to delete book:</b> The book has a link to an another table (probably a series) -- {} ".format( str(e) ) )
else:
st.SetMessage( "<b>Failed to delete book:</b>nbsp; {} ".format( str(e) ) )
return redirect( '/book/{}'.format(id) )
if pid > 0:
return redirect( '/book/{}'.format(pid) )
else:
return redirect( '/' )
# save/update of book
elif book_form.validate():
book = Book.query.get(id)
book.title = request.form['title']
book.owned = request.form['owned']
book.covertype = request.form['covertype']
book.condition = request.form['condition']
book.publisher = request.form['publisher']
book.year_published = request.form['year_published']
book.rating = request.form['rating']
book.notes = request.form['notes']
book.blurb = request.form['blurb']
# set book genre (empty list, add any that are checked on - this allows us to remove unticked ones)
book.genre = []
genre_list = GetGenres()
for genre in genre_list:
if "genre-{}".format(genre.id) in request.form:
book.genre.append( genre )
# set book author (empty list, in form they are in author-0, author-1, ... author-n)
# so use find them all and append them back to now empty list
book.author=[]
for el in request.form:
if 'author-' in el:
book.author.append( Author.query.get( request.form[el] ) )
removing_series=[]
for field in request.form:
if 'removed-book_num' in field:
cnt=int(re.findall( '\d+', field )[0])
removing_series.append( { 'series_id' : request.form['removed-series_id-{}'.format(cnt)] } )
still_in_series=0
if book.IsParent():
for field in request.form:
if 'bsl-book_num-' in field and field != 'bsl-book_num-NUM' and request.form[field] == 'PARENT':
still_in_series=1
# go through children, and force sync any changes of physical parts of parent book
tmp_book=book_schema.dump(book)
for ch_ref in tmp_book['child_ref']:
ch_bid=GetBookIdFromBookSubBookLinkByIdAndSubBookNum( book.id, ch_ref['sub_book_num'] )
child_book=Book.query.get(ch_bid)
child_book.publisher=book.publisher
child_book.owned=book.owned
child_book.covertype=book.covertype
child_book.condition=book.condition
child_book.blurb=book.blurb
if book.IsChild() or (book.IsParent() and not still_in_series):
print ("okay should raise DBox")
print ("{}".format( removing_series ))
if book.IsParent():
CheckSeriesChange={'type':'parent', 'pid': book.id, 'bid': book.id, 'removing_series': removing_series }
else:
CheckSeriesChange={'type':'child', 'pid': book.parent[0].id, 'bid': book.id, 'removing_series': removing_series }
else:
# delete all bsls
db.engine.execute("delete from book_series_link where book_id = {}".format( book.id ) )
cnt=1
for field in request.form:
if 'bsl-book_id-' in field and field != 'bsl-book_id-NUM':
cnt=int(re.findall( '\d+', field )[0])
if book.IsParent():
sql="insert into book_series_link (book_id, series_id) values ( {}, {} )".format( request.form['bsl-book_id-{}'.format(cnt)], request.form['bsl-series_id-{}'.format(cnt)] )
else:
sql="insert into book_series_link (book_id, series_id, book_num) values ( {}, {}, {} )".format( request.form['bsl-book_id-{}'.format(cnt)], request.form['bsl-series_id-{}'.format(cnt)], request.form['bsl-book_num-{}'.format(cnt)])
db.engine.execute( sql )
db.session.commit()
# reset rating on this series as the book has changed (and maybe the rating has changed)
for field in request.form:
if 'bsl-book_id-' in field and field != 'bsl-book_id-NUM':
cnt=int(re.findall( '\d+', field )[0])
s=Series.query.get(request.form['bsl-series_id-{}'.format(cnt)])
s.calcd_rating = CalcAvgRating(s.id)
cnt=cnt+1
db.session.commit()
st.SetMessage( "Successfully Updated Book (id={})".format(id) )
else:
st.SetAlert("danger")
message="Failed to update Book (id={})".format(id)
for field in book_form.errors:
message = "{}<br>{}={}".format( message, field, book_form.errors[field] )
book = Book.query.get(id)
st.SetMessage(message)
else:
book = Book.query.get(id)
if book == None:
st.SetAlert("danger")
st.SetMessage("Cannot find Book (id={})".format(id))
return render_template("base.html", alert=st.GetAlert(), message=st.GetMessage())
book_form=BookForm(obj=book)
author_list = GetAuthors()
genre_list = GetGenres()
book_s = book_schema.dump(book)
return render_template("book.html", b=book, books=book_s, book_form=book_form, author_list=author_list, genre_list=genre_list, page_title=page_title, alert=st.GetAlert(), message=st.GetMessage(), poss_series_list=ListOfSeriesWithMissingBooks(), CheckSeriesChange=CheckSeriesChange)
def GetCount( what, where ):
st="select count(id) as count from book where "
res=db.engine.execute( st + where )
rtn={}
for row in res:
rtn['stat']=what
rtn['value']=row['count']
return rtn
@app.route("/stats", methods=["GET"])
def stats():
stats=[]
stats.append( GetCount( "Num physical Books in DB", "id not in ( select sub_book_id from book_sub_book_link )" ) )
stats.append( GetCount( "Num physical Books owned (aka books on shelf)", "owned=(select id from owned where name = 'Currently Owned') and id not in ( select sub_book_id from book_sub_book_link )" ) )
stats.append( GetCount( "Num physical Books on Wish List", "owned=(select id from owned where name='On Wish List') and id not in ( select sub_book_id from book_sub_book_link )" ) )
stats.append( GetCount( "Num physical Books sold", "owned=(select id from owned where name='Sold') and id not in ( select sub_book_id from book_sub_book_link )" ) )
stats.append( GetCount( "Num all Books in DB", "id>0" ) )
stats.append( GetCount( "Num all Books owned", "owned in (select id from owned where name='Currently Owned')" ) )
stats.append( GetCount( "Num all Books on wish list", "owned in (select id from owned where name='On Wish List')" ) )
stats.append( GetCount( "Num all Books sold", "owned=(select id from owned where name='Sold')" ) )
stats.append( GetCount( "Num all owned Books unrated", "rating in (select id from rating where name in ('N/A', 'Undefined')) and owned = (select id from owned where name='Currently Owned')" ) )
stats.append( GetCount( "Num all Books unrated", "rating in (select id from rating where name in ('N/A', 'Undefined'))" ) )
return render_template("stats.html", stats=stats )
@app.route("/rem_books_from_loan/<id>", methods=["POST"])
def rem_books_from_loan(id):
for field in request.form:
rem_id=int(re.findall( '\d+', field )[0])
try:
db.engine.execute("delete from book_loan_link where book_id = {} and loan_id = {}".format( rem_id, id ))
db.session.commit()
except SQLAlchemyError as e:
st.SetAlert("danger")
st.SetMessage("Failed to remove books from loan! -- {}".format( e.orig ))
return jsonify(success=True)
@app.route("/add_books_to_loan/<id>", methods=["POST"])
def add_books_to_loan(id):
for field in request.form:
add_id=int(re.findall( '\d+', field )[0])
try:
db.engine.execute("insert into book_loan_link (book_id, loan_id) values ( {}, {} )".format( add_id, id ))
print("insert into book_loan_link (book_id, loan_id) values ( {}, {} )".format( add_id, id ))
db.session.commit()
except SQLAlchemyError as e:
st.SetAlert("danger")
st.SetMessage("Failed to add books to loan! -- {}".format( e.orig ))
return jsonify(success=True)
@app.route("/rem_parent_books_from_series/<pid>", methods=["POST"])
def rem_parent_books_from_series(pid):
print ("pid={}".format(pid) )
try:
db.engine.execute("delete from book_series_link where book_id in ( select sub_book_id from book_sub_book_link where book_id = {} ) ".format( pid ))
db.engine.execute("delete from book_series_link where book_id = {}".format( pid ))
db.session.commit()
except SQLAlchemyError as e:
st.SetAlert("danger")
st.SetMessage("Failed to delete parent & sub books from ALL series! -- {}".format( e.orig ))
return jsonify(success=True)
@app.route("/books_on_shelf", methods=["GET"])
def books_on_shelf():
books = Book.query.join(Owned).filter(Owned.name=='Currently Owned').all()
RemSubs(books)
return render_template("books.html", books=books, page_title="Books on Shelf", order_by="Author(s)", show_cols='', hide_cols='' )
@app.route("/unrated_books", methods=["GET"])
def unrated_books():
books = Book.query.join(Condition,Owned).filter(Rating.name=='Undefined',Owned.name=='Currently Owned').all()
return render_template("books.html", books=books, page_title="Books with no rating", show_cols='Rating', hide_cols='' )
def FindMissingBooks():
tmp = db.engine.execute("select s.*, count(bsl.book_num) from book_series_link bsl, series s where bsl.book_num is not null and s.id = bsl.series_id group by s.id order by s.title")
books=[]
sold=Owned.query.filter(Owned.name=='Sold').all()
for t in tmp:
if t.num_books != t.count:
num_sold=0
bsl=Book_Series_Link.query.filter( Book_Series_Link.series_id==t.id ).order_by(Book_Series_Link.book_num).all()
missing=[]
for cnt in range(1,t.num_books+1):
missing.append( cnt )
# check to see if the only books in this series are SOLD, if so, there
# are no missing books here, I clearly dont want more of this series
for b in bsl:
if b.book_num == None:
continue
missing.remove( b.book_num )
tmp_book=Book.query.get(b.book_id)
if tmp_book.owned == sold[0].id:
num_sold = num_sold + 1
if num_sold != t.count:
# turn missing from array into string, and strip 0 and last char (the square brackets)
books.append( { 'id': t.id, 'title': t.title, 'num_books': t.num_books, 'missing': str(missing)[1:-1] } )
return books
@app.route("/missing_books", methods=["GET"])
def missing_books():
books=FindMissingBooks()
return render_template("missing.html", books=books )
@app.route("/wishlist", methods=["GET"])
def wishlist():
books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all()
return render_template("books.html", books=books, page_title="Books On Wish List", show_cols='', hide_cols='Publisher,Condition,Covertype' )
@app.route("/books_to_buy", methods=["GET"])
def books_to_buy():
books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all()
missing = FindMissingBooks()
return render_template("to_buy.html", books=books, missing=missing, page_title="Books To Buy")
@app.route("/needs_replacing", methods=["GET"])
def needs_replacing():
books = Book.query.join(Condition,Owned).filter(Condition.name=='Needs Replacing',Owned.name=='Currently Owned').all()
return render_template("books.html", books=books, page_title="Books that Need Replacing", show_cols='', hide_cols='' )
@app.route("/sold", methods=["GET"])
def sold_books():
books = Book.query.join(Owned).filter(Owned.name=='Sold').all()
return render_template("books.html", books=books, page_title="Books that were Sold", show_cols='', hide_cols='' )
@app.route("/poor_rating", methods=["GET"])
def poor_rating_books():
books = Book.query.join(Rating,Owned).filter(Rating.id>6,Rating.name!='Undefined',Owned.name=='Currently Owned').all()
return render_template("books.html", books=books, page_title="Books that have a Poor Rating (<5 out of 10)", show_cols='Rating', hide_cols='' )
@app.route("/", methods=["GET"])
def main_page():
return render_template("base.html", alert=st.GetAlert(), message=st.GetMessage())
if __name__ == "__main__":
if hostname == DEV_HOST:
app.run(host="0.0.0.0", debug=True)
else:
app.run(ssl_context=('/etc/letsencrypt/live/book.depaoli.id.au/cert.pem', '/etc/letsencrypt/live/book.depaoli.id.au/privkey.pem'), host="0.0.0.0", debug=False)