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 ####################################### CLASSES / DB model ####################################### class QuickParentBook: parent=[] def __repr__(self): return "".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 "".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 "".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 "".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 "".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/", 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/", 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/", 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/ -- 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/ -- 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() 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']) print( parent.series ) if len(parent.series) > 0: print ("I think this means we have added a sub-book to something in a series already" ) 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 = "{}
{}={}".format( message, field, form.errors[field] ) if len(book_genres) == 0: message = "{}
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/", 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 len(book.child_ref) > 0: 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("warning") st.SetMessage("WARNING: Deleting being tested at present.
") st.AppendMessage("Deleted {}".format(book.title) ) pid = 0 if len(book.parent) > 0: 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( "Failed to delete book: The book has a link to an another table (probably a series) -- {} ".format( str(e) ) ) else: st.SetMessage( "Failed to delete book:nbsp; {} ".format( str(e) ) ) return redirect( '/book/{}'.format(id) ) if pid > 0: return redirect( '/book/{}'.format(pid) ) else: return redirect( '/' ) 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 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 ) 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 = "{}
{}={}".format( message, field, book_form.errors[field] ) book = Book.query.get(id) st.SetMessage(message) else: book = Book.query.get(id) 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/", 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/", 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/", 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='' ) @app.route("/missing_books", methods=["GET"]) def missing_books(): 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: 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: print( "Seems that all the books in this {} are Sold, should not list it".format(t.title)) else: # 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 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("/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)