Files
books/main.py

313 lines
15 KiB
Python

from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
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
app = Flask(__name__)
### what is this value? I gather I should chagne it?
DB_URL = 'postgresql+psycopg2://ddp:NWNlfa01@127.0.0.1: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)
from author import Author, AuthorForm, AuthorSchema, GetAuthors
from publisher import Publisher, PublisherForm, PublisherSchema, GetPublishers
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
from loan import Loan, LoanForm, LoanSchema
from series import Series, SeriesForm, SeriesSchema
####################################### CLASSES / DB model #######################################
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_publisher_link = db.Table('book_publisher_link', db.Model.metadata,
db.Column('book_id', db.Integer, db.ForeignKey('book.id')),
db.Column('publisher_id', db.Integer, db.ForeignKey('publisher.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, unique=True, nullable=False, primary_key=True)
title = db.Column(db.String(100), unique=True, nullable=False)
author = db.relationship('Author', secondary=book_author_link)
publisher = db.relationship('Publisher', secondary=book_publisher_link)
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__);
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.Date)
modified = db.Column(db.Date)
parent_ref = db.relationship('Book_Sub_Book_Link', 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" )
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" )
def IsParent(self):
if len(self.child_ref):
return True
else:
return False
def IsChild(self):
if len(self.parent_ref):
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():
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 ):
print( "Moving {} by {}".format( self.title, amt ) )
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
def __repr__(self):
return "<id: {}, author: {}, title: {}, year_published: {}, rating: {}, condition: {}, owned: {}, covertype: {}, notes: {}, blurb: {}, created: {}, modified: {}, publisher: {}>".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 )
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)
publisher = ma.Nested(PublisherSchema, many=True)
genre = ma.Nested(GenreSchema, many=True)
loan = ma.Nested(LoanSchema, many=True)
series = ma.Nested(SeriesSchema, many=True)
parent_ref = ma.Nested(Book_Sub_Book_LinkSchema, 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
# publiser built by hand
# genre built by hand
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.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:')
# allow jinja2 to call this python function
app.jinja_env.globals['GetCovertypeById'] = GetCovertypeById
app.jinja_env.globals['GetOwnedById'] = GetOwnedById
app.jinja_env.globals['GetConditionById'] = GetConditionById
app.jinja_env.globals['SeriesBookNum'] = SeriesBookNum
### DDP: do I need many=True on Author as books have many authors? (or in BookSchema declaration above?)
book_schema = BookSchema()
books_schema = BookSchema(many=True)
################################# 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()
return tmp_book[0].book_id
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
####################################### ROUTES #######################################
@app.route("/books", methods=["GET"])
def books():
books = Book.query.all()
# ignore ORM, its too slow. 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
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)
books[index].parent_id = row.book_id
books[index].sub_book_num = row.sub_book_num
return render_template("books.html", books=books )
@app.route("/books_for_loan/<id>", methods=["GET"])
def books_for_loan(id):
books = Book.query.join(Book_Loan_Link).filter(Book_Loan_Link.loan_id==id).order_by(Book.id).all()
return render_template("books_for_loan.html", books=books)
@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)
# 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 )
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_ref[0].book_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 )
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("/book/<id>", methods=["GET"])
def book(id):
book = Book.query.get(id)
book_s = book_schema.dump(book)
####################################
# 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, book.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 where bsb.book_id = {} and book.id = bsb.sub_book_id and book.id = bal.book_id and bal.author_id = author.id".format( id ) )
sub_book=[]
for row in subs:
# get genres for sub book and add by hand first
tmp_g = []
genres = db.engine.execute ( "select genre.id, genre.name from genre, book_genre_link bgl where genre.id = bgl.genre_id and bgl.book_id = {}".format( row.sub_book_id ) )
for genre in genres:
tmp_g.append( { 'id': genre.id, 'name': genre.name } )
sub_book.append( { '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, 'genres' : tmp_g } )
book_s['sub_book'] = sub_book
book_form=BookForm(request.form)
# set defaults for drop-down's based on this book
book_form.condition.default = book.condition
book_form.covertype.default = book.covertype
book_form.owned.default = book.owned
book_form.rating.default = book.rating
book_form.process()
author_list = GetAuthors()
genre_list = GetGenres()
publisher_list = GetPublishers()
return render_template("book.html", books=book_s, subs=sub_book, book_form=book_form, author_list=author_list, publisher_list=publisher_list, genre_list=genre_list )
@app.route("/", methods=["GET"])
def main_page():
return render_template("base.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)