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 "".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.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" ) 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" ) 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 "".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.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) 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() 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 ####################################### 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/", 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_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("/subbooks_for_book/", methods=["GET", "POST"]) def subbooks_for_book(id): print( "called subbooks_for_book: {}".format(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"]) def book(id): book = Book.query.get(id) book_s = book_schema.dump(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", b=book, books=book_s, 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)