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 from status import st, Status ####################################### Flask App globals ####################################### 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) ################################# 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 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_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, 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.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__); 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) # 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" ) 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 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 "".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.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' ) ################################# 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 ####################################### 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['SeriesBookNum'] = SeriesBookNum app.jinja_env.globals['ClearStatus'] = st.ClearMessage book_schema = BookSchema() ####################################### ROUTES ####################################### @app.route("/search", methods=["POST"]) def search(): books = Book.query.filter(Book.title.ilike("%{}%".format(request.form['term']))).all() AddSubs(books) return render_template("books.html", books=books) @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"]) 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/", 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 ) # 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 ) 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 ) @app.route("/book/", methods=["GET", "POST"]) def book(id): alert="success" message="" book_form = BookForm(request.form) book = Book.query.get(id) if request.method == 'POST': if 'delete' in request.form: alert="danger" message="Sorry, Deleting unsupported at present" elif book_form.validate(): 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 while to find them all and append them back to now empty list acnt=0 book.author=[] while "author-{}".format( acnt ) in request.form: book.author.append( Author.query.get( request.form["author-{}".format( acnt )] ) ) acnt = acnt + 1 ## TODO: # what about add/remove author, series?, subbooks?, loan?, etc. db.session.commit() message="Successfully Updated Book (id={})".format(id) else: alert="danger" print( book_form) message="Err... Failed to update Book (id={})".format(id) else: 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, alert=alert, message=message, n=book_form.notes ) @app.route("/", methods=["GET"]) def main_page(): return render_template("base.html") if __name__ == "__main__": app.run(host="0.0.0.0", debug=True)