make all this have a login page, use ldap, put a logo on, moved some upstream stuff to static/ -- need to do more here to be consistent with bootstrap 5, but for another day

This commit is contained in:
2022-06-19 22:45:54 +10:00
parent 4725f006bc
commit c29f73f8ab
14 changed files with 274 additions and 9 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
__pycache__/ __pycache__/
DB_BACKUP/
static/

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, validators, Form from wtforms import SubmitField, StringField, HiddenField, validators, Form
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -41,6 +42,7 @@ class AuthorForm(FlaskForm):
# /authors -> GET only -> prints out list of all authors # /authors -> GET only -> prints out list of all authors
################################################################################ ################################################################################
@app.route("/authors", methods=["GET"]) @app.route("/authors", methods=["GET"])
@login_required
def authors(): def authors():
authors = Author.query.all() authors = Author.query.all()
return render_template("authors.html", authors=authors, alert=st.GetAlert(), message=st.GetMessage() ) return render_template("authors.html", authors=authors, alert=st.GetAlert(), message=st.GetMessage() )
@@ -50,6 +52,7 @@ def authors():
# /author -> GET/POST -> creates a new author type and when created, takes you back to /authors # /author -> GET/POST -> creates a new author type and when created, takes you back to /authors
################################################################################ ################################################################################
@app.route("/author", methods=["GET", "POST"]) @app.route("/author", methods=["GET", "POST"])
@login_required
def new_author(): def new_author():
form = AuthorForm(request.form) form = AuthorForm(request.form)
page_title='Create new Author' page_title='Create new Author'
@@ -71,6 +74,7 @@ def new_author():
# /author/<id> -> GET/POST(save or delete) -> shows/edits/delets a single author # /author/<id> -> GET/POST(save or delete) -> shows/edits/delets a single author
################################################################################ ################################################################################
@app.route("/author/<id>", methods=["GET", "POST"]) @app.route("/author/<id>", methods=["GET", "POST"])
@login_required
def author(id): def author(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = AuthorForm(request.form) form = AuthorForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, SelectField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, validators
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -37,6 +38,7 @@ class ConditionForm(FlaskForm):
# /conditions -> GET only -> prints out list of all conditions # /conditions -> GET only -> prints out list of all conditions
################################################################################ ################################################################################
@app.route("/conditions", methods=["GET"]) @app.route("/conditions", methods=["GET"])
@login_required
def conditions(): def conditions():
objects = Condition.query.order_by('id').all() objects = Condition.query.order_by('id').all()
return render_template("show_id_name.html", objects=objects, page_title='Show All Conditions', url_base='condition', alert=st.GetAlert(), message=st.GetMessage() ) return render_template("show_id_name.html", objects=objects, page_title='Show All Conditions', url_base='condition', alert=st.GetAlert(), message=st.GetMessage() )
@@ -45,6 +47,7 @@ def conditions():
# /condition -> GET/POST -> creates a new condition type and when created, takes you back to /conditions # /condition -> GET/POST -> creates a new condition type and when created, takes you back to /conditions
################################################################################ ################################################################################
@app.route("/condition", methods=["GET", "POST"]) @app.route("/condition", methods=["GET", "POST"])
@login_required
def new_condition(): def new_condition():
form = ConditionForm(request.form) form = ConditionForm(request.form)
page_title='Create new Condition' page_title='Create new Condition'
@@ -66,6 +69,7 @@ def new_condition():
# /condition/<id> -> GET/POST(save or delete) -> shows/edits/delets a single condition # /condition/<id> -> GET/POST(save or delete) -> shows/edits/delets a single condition
################################################################################ ################################################################################
@app.route("/condition/<id>", methods=["GET", "POST"]) @app.route("/condition/<id>", methods=["GET", "POST"])
@login_required
def condition(id): def condition(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = ConditionForm(request.form) form = ConditionForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, SelectField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, validators
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -37,6 +38,7 @@ class CovertypeForm(FlaskForm):
# /covertypes -> GET only -> prints out list of all covertypes # /covertypes -> GET only -> prints out list of all covertypes
################################################################################ ################################################################################
@app.route("/covertypes", methods=["GET"]) @app.route("/covertypes", methods=["GET"])
@login_required
def covertypes(): def covertypes():
objects = Covertype.query.order_by('id').all() objects = Covertype.query.order_by('id').all()
return render_template("show_id_name.html", objects=objects, page_title='Show All Covertypes', url_base='covertype', alert=st.GetAlert(), message=st.GetMessage() ) return render_template("show_id_name.html", objects=objects, page_title='Show All Covertypes', url_base='covertype', alert=st.GetAlert(), message=st.GetMessage() )
@@ -45,6 +47,7 @@ def covertypes():
# /covertype -> GET/POST -> creates a new covertype type and when created, takes you back to /covertypes # /covertype -> GET/POST -> creates a new covertype type and when created, takes you back to /covertypes
################################################################################ ################################################################################
@app.route("/covertype", methods=["GET", "POST"]) @app.route("/covertype", methods=["GET", "POST"])
@login_required
def new_covertype(): def new_covertype():
form = CovertypeForm(request.form) form = CovertypeForm(request.form)
page_title='Create new Covertype' page_title='Create new Covertype'
@@ -67,6 +70,7 @@ def new_covertype():
# /covertype/<id> -> GET/POST(save or delete) -> shows/edits/delets a single covertype # /covertype/<id> -> GET/POST(save or delete) -> shows/edits/delets a single covertype
################################################################################ ################################################################################
@app.route("/covertype/<id>", methods=["GET", "POST"]) @app.route("/covertype/<id>", methods=["GET", "POST"])
@login_required
def covertype(id): def covertype(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = CovertypeForm(request.form) form = CovertypeForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, SelectField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, validators
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import func, Sequence from sqlalchemy import func, Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -37,6 +38,7 @@ class GenreForm(FlaskForm):
# /genres -> GET only -> prints out list of all genres # /genres -> GET only -> prints out list of all genres
################################################################################ ################################################################################
@app.route("/genres", methods=["GET"]) @app.route("/genres", methods=["GET"])
@login_required
def genres(): def genres():
objects = Genre.query.order_by('id').all() objects = Genre.query.order_by('id').all()
return render_template("show_id_name.html", objects=objects, page_title='Show All Genres', url_base='genre', alert=st.GetAlert(), message=st.GetMessage() ) return render_template("show_id_name.html", objects=objects, page_title='Show All Genres', url_base='genre', alert=st.GetAlert(), message=st.GetMessage() )
@@ -45,6 +47,7 @@ def genres():
# /genre -> GET/POST -> creates a new genre type and when created, takes you back to /genres # /genre -> GET/POST -> creates a new genre type and when created, takes you back to /genres
################################################################################ ################################################################################
@app.route("/genre", methods=["GET", "POST"]) @app.route("/genre", methods=["GET", "POST"])
@login_required
def new_genre(): def new_genre():
form = GenreForm(request.form) form = GenreForm(request.form)
page_title='Create new Genre' page_title='Create new Genre'
@@ -66,6 +69,7 @@ def new_genre():
# /genre/<id> -> GET/POST(save or delete) -> shows/edits/delets a single genre # /genre/<id> -> GET/POST(save or delete) -> shows/edits/delets a single genre
################################################################################ ################################################################################
@app.route("/genre/<id>", methods=["GET", "POST"]) @app.route("/genre/<id>", methods=["GET", "POST"])
@login_required
def genre(id): def genre(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = GenreForm(request.form) form = GenreForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, validators, TextAreaField from wtforms import SubmitField, StringField, HiddenField, validators, TextAreaField
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_login import login_required, current_user
from wtforms import DateField from wtforms import DateField
from main import db, app, ma from main import db, app, ma
from datetime import date from datetime import date
@@ -48,6 +49,7 @@ class LoanForm(FlaskForm):
# /loans -> GET only -> prints out list of all loans # /loans -> GET only -> prints out list of all loans
################################################################################ ################################################################################
@app.route("/loans", methods=["GET"]) @app.route("/loans", methods=["GET"])
@login_required
def loans(): def loans():
loans = Loan.query.all() loans = Loan.query.all()
return render_template("loans.html", loans=loans, alert=st.GetAlert(), message=st.GetMessage()) return render_template("loans.html", loans=loans, alert=st.GetAlert(), message=st.GetMessage())
@@ -56,6 +58,7 @@ def loans():
# /loan -> GET/POST -> creates a new loan type and when created, takes you back to /loans # /loan -> GET/POST -> creates a new loan type and when created, takes you back to /loans
################################################################################ ################################################################################
@app.route("/loan", methods=["GET", "POST"]) @app.route("/loan", methods=["GET", "POST"])
@login_required
def new_loan(): def new_loan():
form = LoanForm(request.form) form = LoanForm(request.form)
page_title='Create new Loan' page_title='Create new Loan'
@@ -78,6 +81,7 @@ def new_loan():
# /loan/<id> -> GET/POST(save or delete) -> shows/edits/delets a single loan # /loan/<id> -> GET/POST(save or delete) -> shows/edits/delets a single loan
################################################################################ ################################################################################
@app.route("/loan/<id>", methods=["GET", "POST"]) @app.route("/loan/<id>", methods=["GET", "POST"])
@login_required
def loan(id): def loan(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = LoanForm(request.form) form = LoanForm(request.form)

121
main.py
View File

@@ -1,11 +1,16 @@
from flask import Flask, render_template, request, redirect, jsonify from flask import Flask, render_template, request, redirect, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from flask_marshmallow import Marshmallow from flask_marshmallow import Marshmallow
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
from wtforms import SubmitField, StringField, HiddenField, SelectField, IntegerField, TextAreaField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, IntegerField, TextAreaField, validators
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_compress import Compress
from status import st, Status from status import st, Status
# for ldap auth
from flask_ldap3_login import LDAP3LoginManager
from flask_login import LoginManager, login_user, login_required, UserMixin, current_user, logout_user
from flask_ldap3_login.forms import LDAPLoginForm
import re import re
import os import os
@@ -21,10 +26,30 @@ else:
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G') app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
# ldap config vars: (the last one is required, or python ldap freaks out)
app.config['LDAP_HOST'] = 'mara.ddp.net'
app.config['LDAP_BASE_DN'] = 'dc=depaoli,dc=id,dc=au'
app.config['LDAP_USER_DN'] = 'ou=users'
app.config['LDAP_GROUP_DN'] = 'ou=groups'
app.config['LDAP_USER_RDN_ATTR'] = 'cn'
app.config['LDAP_USER_LOGIN_ATTR'] = 'uid'
app.config['LDAP_BIND_USER_DN'] = None
app.config['LDAP_BIND_USER_PASSWORD'] = None
app.config['LDAP_GROUP_OBJECT_FILTER'] = '(objectclass=posixGroup)'
db = SQLAlchemy(app) db = SQLAlchemy(app)
ma = Marshmallow(app) ma = Marshmallow(app)
Bootstrap(app) Bootstrap(app)
# setup ldap for auth
login_manager = LoginManager(app) # Setup a Flask-Login Manager
ldap_manager = LDAP3LoginManager(app) # Setup a LDAP3 Login Manager.
login_manager.login_view = "login" # default login route, failed with url_for, so hard-coded
# enable compression for http / speed
Compress(app)
################################# Now, import non-book classes ################################### ################################# Now, import non-book classes ###################################
from author import Author, AuthorForm, AuthorSchema, GetAuthors from author import Author, AuthorForm, AuthorSchema, GetAuthors
from publisher import Publisher, PublisherForm, PublisherSchema, GetPublisherById from publisher import Publisher, PublisherForm, PublisherSchema, GetPublisherById
@@ -35,6 +60,7 @@ from owned import Owned, OwnedForm, OwnedSchema, GetOwnedById
from rating import Rating, RatingForm, RatingSchema, GetRatingById from rating import Rating, RatingForm, RatingSchema, GetRatingById
from loan import Loan, LoanForm, LoanSchema from loan import Loan, LoanForm, LoanSchema
from series import Series, SeriesForm, SeriesSchema, ListOfSeriesWithMissingBooks, CalcAvgRating from series import Series, SeriesForm, SeriesSchema, ListOfSeriesWithMissingBooks, CalcAvgRating
from user import BDBUser
####################################### CLASSES / DB model ####################################### ####################################### CLASSES / DB model #######################################
class QuickParentBook: class QuickParentBook:
@@ -246,12 +272,67 @@ app.jinja_env.globals['GetRatingById'] = GetRatingById
app.jinja_env.globals['SeriesBookNum'] = SeriesBookNum app.jinja_env.globals['SeriesBookNum'] = SeriesBookNum
app.jinja_env.globals['ClearStatus'] = st.ClearStatus app.jinja_env.globals['ClearStatus'] = st.ClearStatus
app.jinja_env.globals['ListOfSeriesWithMissingBooks'] = ListOfSeriesWithMissingBooks app.jinja_env.globals['ListOfSeriesWithMissingBooks'] = ListOfSeriesWithMissingBooks
app.jinja_env.globals['current_user'] = current_user
book_schema = BookSchema() book_schema = BookSchema()
books_schema = BookSchema(many=True) books_schema = BookSchema(many=True)
# Declare a User Loader for Flask-Login.
# Returns the User if it exists in our 'database', otherwise returns None.
@login_manager.user_loader
def load_user(id):
bdbu=BDBUser.query.filter(BDBUser.dn==id).first()
return bdbu
# Declare The User Saver for Flask-Ldap3-Login
# This method is called whenever a LDAPLoginForm() successfully validates.
# store the user details / session in the DB if it is not in there already
@ldap_manager.save_user
def save_user(dn, username, data, memberships):
bdbu=BDBUser.query.filter(BDBUser.dn==dn).first()
# if we already have a valid user/session, and say the web has restarted, just re-use it, dont make more users
if bdbu:
return bdbu
bdbu=BDBUser(dn=dn)
db.session.add(bdbu)
db.session.commit()
return bdbu
# POST is when user submits pwd & uses flask-login to hit ldap, validate pwd
# if valid, then we save user/session into the DB via login_user() -> calls save_user()
@app.route('/login', methods=['GET', 'POST'])
def login():
# Instantiate a LDAPLoginForm which has a validator to check if the user
# exists in LDAP.
form = LDAPLoginForm()
form.submit.label.text="Login"
# the re matches on any special LDAP chars, we dont want someone
# ldap-injecting our username, so send them back to the login page instead
if request.method == 'POST' and re.search( r'[()\\*&!]', request.form['username']):
print( f"WARNING: Detected special LDAP chars in username: {request.form['username']}")
return redirect(url_for('login'))
if form.validate_on_submit():
# Successfully logged in, We can now access the saved user object via form.user.
login_user(form.user, remember=True) # Tell flask-login to log them in.
next = request.args.get("next")
if next:
return redirect(next) # Send them back where they came from
else:
return redirect( url_for('main_page') )
return render_template("login.html", form=form)
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect('/login')
####################################### ROUTES ####################################### ####################################### ROUTES #######################################
@app.route("/search", methods=["POST"]) @app.route("/search", methods=["POST"])
@login_required
def search(): def search():
if 'InDBox' in request.form: if 'InDBox' in request.form:
# removes already loaned books from list of books to loan out # removes already loaned books from list of books to loan out
@@ -264,12 +345,14 @@ def search():
return render_template("books.html", books=books, InDBox=InDBox) return render_template("books.html", books=books, InDBox=InDBox)
@app.route("/books", methods=["GET"]) @app.route("/books", methods=["GET"])
@login_required
def books(): def books():
books = Book.query.all() books = Book.query.all()
AddSubs(books) AddSubs(books)
return render_template("books.html", books=books ) return render_template("books.html", books=books )
@app.route("/books_for_loan/<id>", methods=["GET", "POST"]) @app.route("/books_for_loan/<id>", methods=["GET", "POST"])
@login_required
def books_for_loan(id): def books_for_loan(id):
books = Book.query.join(Book_Loan_Link).filter(Book_Loan_Link.loan_id==id).order_by(Book.id).all() books = Book.query.join(Book_Loan_Link).filter(Book_Loan_Link.loan_id==id).order_by(Book.id).all()
if request.method == 'POST': if request.method == 'POST':
@@ -332,6 +415,7 @@ def CalcMoveForBookInSeries( id, bid, dir ):
book2.MoveBookInSeries( id, b2_move_by ) book2.MoveBookInSeries( id, b2_move_by )
@app.route("/books_for_series/<id>", methods=["GET", "POST"]) @app.route("/books_for_series/<id>", methods=["GET", "POST"])
@login_required
def books_for_series(id): def books_for_series(id):
if request.method == 'POST': if request.method == 'POST':
if 'move_button' in request.form: if 'move_button' in request.form:
@@ -346,6 +430,7 @@ def books_for_series(id):
return render_template("books_for_series.html", books=books, series=series) return render_template("books_for_series.html", books=books, series=series)
@app.route("/subbooks_for_book/<id>", methods=["GET", "POST"]) @app.route("/subbooks_for_book/<id>", methods=["GET", "POST"])
@login_required
def subbooks_for_book(id): def subbooks_for_book(id):
if request.method == 'POST': if request.method == 'POST':
if 'move_button' in request.form: if 'move_button' in request.form:
@@ -389,6 +474,7 @@ def subbooks_for_book(id):
# /books (or /book/<parent_id> -- if you added a sub-book of parent_id # /books (or /book/<parent_id> -- if you added a sub-book of parent_id
################################################################################ ################################################################################
@app.route("/remove_subbook", methods=["POST"]) @app.route("/remove_subbook", methods=["POST"])
@login_required
def remove_sub_book(): def remove_sub_book():
try: 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.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']) )
@@ -404,6 +490,7 @@ def remove_sub_book():
# /books (or /book/<parent_id> -- if you added a sub-book of parent_id # /books (or /book/<parent_id> -- if you added a sub-book of parent_id
################################################################################ ################################################################################
@app.route("/book", methods=["GET", "POST"]) @app.route("/book", methods=["GET", "POST"])
@login_required
def new_book(): def new_book():
form = BookForm(request.form) form = BookForm(request.form)
page_title='Create new Book' page_title='Create new Book'
@@ -475,6 +562,7 @@ def new_book():
@app.route("/book/<id>", methods=["GET", "POST"]) @app.route("/book/<id>", methods=["GET", "POST"])
@login_required
def book(id): def book(id):
book_form = BookForm(request.form) book_form = BookForm(request.form)
page_title='Edit Book' page_title='Edit Book'
@@ -619,6 +707,7 @@ def GetCount( what, where ):
return rtn return rtn
@app.route("/stats", methods=["GET"]) @app.route("/stats", methods=["GET"])
@login_required
def stats(): def stats():
stats=[] stats=[]
@@ -637,6 +726,7 @@ def stats():
return render_template("stats.html", stats=stats ) return render_template("stats.html", stats=stats )
@app.route("/rem_books_from_loan/<id>", methods=["POST"]) @app.route("/rem_books_from_loan/<id>", methods=["POST"])
@login_required
def rem_books_from_loan(id): def rem_books_from_loan(id):
for field in request.form: for field in request.form:
rem_id=int(re.findall( '\d+', field )[0]) rem_id=int(re.findall( '\d+', field )[0])
@@ -649,6 +739,7 @@ def rem_books_from_loan(id):
return jsonify(success=True) return jsonify(success=True)
@app.route("/add_books_to_loan/<id>", methods=["POST"]) @app.route("/add_books_to_loan/<id>", methods=["POST"])
@login_required
def add_books_to_loan(id): def add_books_to_loan(id):
for field in request.form: for field in request.form:
add_id=int(re.findall( '\d+', field )[0]) add_id=int(re.findall( '\d+', field )[0])
@@ -662,6 +753,7 @@ def add_books_to_loan(id):
return jsonify(success=True) return jsonify(success=True)
@app.route("/rem_parent_books_from_series/<pid>", methods=["POST"]) @app.route("/rem_parent_books_from_series/<pid>", methods=["POST"])
@login_required
def rem_parent_books_from_series(pid): def rem_parent_books_from_series(pid):
print ("pid={}".format(pid) ) print ("pid={}".format(pid) )
try: try:
@@ -674,12 +766,14 @@ def rem_parent_books_from_series(pid):
return jsonify(success=True) return jsonify(success=True)
@app.route("/books_on_shelf", methods=["GET"]) @app.route("/books_on_shelf", methods=["GET"])
@login_required
def books_on_shelf(): def books_on_shelf():
books = Book.query.join(Owned).filter(Owned.name=='Currently Owned').all() books = Book.query.join(Owned).filter(Owned.name=='Currently Owned').all()
RemSubs(books) RemSubs(books)
return render_template("books.html", books=books, page_title="Books on Shelf", order_by="Author(s)", show_cols='', hide_cols='' ) 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"]) @app.route("/unrated_books", methods=["GET"])
@login_required
def unrated_books(): def unrated_books():
books = Book.query.join(Condition,Owned).filter(Rating.name=='Undefined',Owned.name=='Currently Owned').all() 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='' ) return render_template("books.html", books=books, page_title="Books with no rating", show_cols='Rating', hide_cols='' )
@@ -710,39 +804,50 @@ def FindMissingBooks():
return books return books
@app.route("/missing_books", methods=["GET"]) @app.route("/missing_books", methods=["GET"])
@login_required
def missing_books(): def missing_books():
books=FindMissingBooks() books=FindMissingBooks()
return render_template("missing.html", books=books ) return render_template("missing.html", books=books )
@app.route("/wishlist", methods=["GET"]) @app.route("/wishlist", methods=["GET"])
@login_required
def wishlist(): def wishlist():
books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all() 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' ) 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"]) @app.route("/books_to_buy", methods=["GET"])
@login_required
def books_to_buy(): def books_to_buy():
books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all() books = Book.query.join(Owned).filter(Owned.name=='On Wish List').all()
missing = FindMissingBooks() missing = FindMissingBooks()
return render_template("to_buy.html", books=books, missing=missing, page_title="Books To Buy") return render_template("to_buy.html", books=books, missing=missing, page_title="Books To Buy")
@app.route("/needs_replacing", methods=["GET"]) @app.route("/needs_replacing", methods=["GET"])
@login_required
def needs_replacing(): def needs_replacing():
books = Book.query.join(Condition,Owned).filter(Condition.name=='Needs Replacing',Owned.name=='Currently Owned').all() 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='' ) return render_template("books.html", books=books, page_title="Books that Need Replacing", show_cols='', hide_cols='' )
@app.route("/sold", methods=["GET"]) @app.route("/sold", methods=["GET"])
@login_required
def sold_books(): def sold_books():
books = Book.query.join(Owned).filter(Owned.name=='Sold').all() 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='' ) return render_template("books.html", books=books, page_title="Books that were Sold", show_cols='', hide_cols='' )
@app.route("/poor_rating", methods=["GET"]) @app.route("/poor_rating", methods=["GET"])
@login_required
def poor_rating_books(): def poor_rating_books():
books = Book.query.join(Rating,Owned).filter(Rating.id>6,Rating.name!='Undefined',Owned.name=='Currently Owned').all() 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='' ) return render_template("books.html", books=books, page_title="Books that have a Poor Rating (<5 out of 10)", show_cols='Rating', hide_cols='' )
# default page, just the navbar
@app.route("/", methods=["GET"]) @app.route("/", methods=["GET"])
@login_required
def main_page(): def main_page():
# Redirect users who are not logged in.
if not current_user or current_user.is_anonymous:
return redirect(url_for('login'))
return render_template("base.html", alert=st.GetAlert(), message=st.GetMessage()) return render_template("base.html", alert=st.GetAlert(), message=st.GetMessage())
if __name__ == "__main__": if __name__ == "__main__":
@@ -750,3 +855,17 @@ if __name__ == "__main__":
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) 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)
else: else:
app.run(host="0.0.0.0", debug=True) app.run(host="0.0.0.0", debug=True)
###############################################################################
# This func creates a new filter in jinja2 to test to hand back the username
# from the ldap dn
################################################################################
@app.template_filter('Username')
def _jinja2_filter_parentpath(dn):
# pull apart a dn (uid=xxx,dn=yyy,etc), and return the xxx
username=str(dn)
s=username.index('=')
s+=1
f=username.index(',')
return username[s:f]

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, SelectField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, validators
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import func, Sequence from sqlalchemy import func, Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -37,6 +38,7 @@ class OwnedForm(FlaskForm):
# /owneds -> GET only -> prints out list of all owneds # /owneds -> GET only -> prints out list of all owneds
################################################################################ ################################################################################
@app.route("/owneds", methods=["GET"]) @app.route("/owneds", methods=["GET"])
@login_required
def owneds(): def owneds():
objects = Owned.query.order_by('id').all() objects = Owned.query.order_by('id').all()
return render_template("show_id_name.html", objects=objects, page_title='Show All Ownership types', url_base='owned', alert=st.GetAlert(), message=st.GetMessage() ) return render_template("show_id_name.html", objects=objects, page_title='Show All Ownership types', url_base='owned', alert=st.GetAlert(), message=st.GetMessage() )
@@ -45,6 +47,7 @@ def owneds():
# /owned -> GET/POST -> creates a new owned type and when created, takes you back to /owneds # /owned -> GET/POST -> creates a new owned type and when created, takes you back to /owneds
################################################################################ ################################################################################
@app.route("/owned", methods=["GET", "POST"]) @app.route("/owned", methods=["GET", "POST"])
@login_required
def new_owned(): def new_owned():
form = OwnedForm(request.form) form = OwnedForm(request.form)
page_title='Create new Ownership Type' page_title='Create new Ownership Type'
@@ -66,6 +69,7 @@ def new_owned():
# /owned/<id> -> GET/POST(save or delete) -> shows/edits/delets a single owned # /owned/<id> -> GET/POST(save or delete) -> shows/edits/delets a single owned
################################################################################ ################################################################################
@app.route("/owned/<id>", methods=["GET", "POST"]) @app.route("/owned/<id>", methods=["GET", "POST"])
@login_required
def owned(id): def owned(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = OwnedForm(request.form) form = OwnedForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, SelectField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, validators
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -37,6 +38,7 @@ class PublisherForm(FlaskForm):
# /publishers -> GET only -> prints out list of all publishers # /publishers -> GET only -> prints out list of all publishers
################################################################################ ################################################################################
@app.route("/publishers", methods=["GET"]) @app.route("/publishers", methods=["GET"])
@login_required
def publishers(): def publishers():
objects = Publisher.query.order_by('id').all() objects = Publisher.query.order_by('id').all()
return render_template("show_id_name.html", objects=objects, page_title='Show All Publishers', url_base='publisher', alert=st.GetAlert(), message=st.GetMessage() ) return render_template("show_id_name.html", objects=objects, page_title='Show All Publishers', url_base='publisher', alert=st.GetAlert(), message=st.GetMessage() )
@@ -45,6 +47,7 @@ def publishers():
# /publisher -> GET/POST -> creates a new publisher type and when created, takes you back to /publishers # /publisher -> GET/POST -> creates a new publisher type and when created, takes you back to /publishers
################################################################################ ################################################################################
@app.route("/publisher", methods=["GET", "POST"]) @app.route("/publisher", methods=["GET", "POST"])
@login_required
def new_publisher(): def new_publisher():
form = PublisherForm(request.form) form = PublisherForm(request.form)
page_title='Create new Publisher' page_title='Create new Publisher'
@@ -66,6 +69,7 @@ def new_publisher():
# /publisher/<id> -> GET/POST(save or delete) -> shows/edits/delets a single publisher # /publisher/<id> -> GET/POST(save or delete) -> shows/edits/delets a single publisher
################################################################################ ################################################################################
@app.route("/publisher/<id>", methods=["GET", "POST"]) @app.route("/publisher/<id>", methods=["GET", "POST"])
@login_required
def publisher(id): def publisher(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = PublisherForm(request.form) form = PublisherForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, SelectField, validators from wtforms import SubmitField, StringField, HiddenField, SelectField, validators
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_login import login_required, current_user
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -37,6 +38,7 @@ class RatingForm(FlaskForm):
# /ratings -> GET only -> prints out list of all ratings # /ratings -> GET only -> prints out list of all ratings
################################################################################ ################################################################################
@app.route("/ratings", methods=["GET"]) @app.route("/ratings", methods=["GET"])
@login_required
def ratings(): def ratings():
objects = Rating.query.order_by('id').all() objects = Rating.query.order_by('id').all()
return render_template("show_id_name.html", objects=objects, page_title='Show All Ratings', url_base='rating', alert=st.GetAlert(), message=st.GetMessage() ) return render_template("show_id_name.html", objects=objects, page_title='Show All Ratings', url_base='rating', alert=st.GetAlert(), message=st.GetMessage() )
@@ -45,6 +47,7 @@ def ratings():
# /rating -> GET/POST -> creates a new rating type and when created, takes you back to /ratings # /rating -> GET/POST -> creates a new rating type and when created, takes you back to /ratings
################################################################################ ################################################################################
@app.route("/rating", methods=["GET", "POST"]) @app.route("/rating", methods=["GET", "POST"])
@login_required
def new_rating(): def new_rating():
form = RatingForm(request.form) form = RatingForm(request.form)
page_title='Create new Rating' page_title='Create new Rating'
@@ -66,6 +69,7 @@ def new_rating():
# /rating/<id> -> GET/POST(save or delete) -> shows/edits/delets a single rating # /rating/<id> -> GET/POST(save or delete) -> shows/edits/delets a single rating
################################################################################ ################################################################################
@app.route("/rating/<id>", methods=["GET", "POST"]) @app.route("/rating/<id>", methods=["GET", "POST"])
@login_required
def rating(id): def rating(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = RatingForm(request.form) form = RatingForm(request.form)

View File

@@ -1,6 +1,7 @@
from wtforms import SubmitField, StringField, HiddenField, validators, TextAreaField, IntegerField from wtforms import SubmitField, StringField, HiddenField, validators, TextAreaField, IntegerField
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask import request, render_template, redirect from flask import request, render_template, redirect
from flask_login import login_required, current_user
from wtforms import DateField from wtforms import DateField
from main import db, app, ma from main import db, app, ma
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -58,6 +59,7 @@ def ListOfSeriesWithMissingBooks():
# /seriess -> GET only -> prints out list of all seriess # /seriess -> GET only -> prints out list of all seriess
################################################################################ ################################################################################
@app.route("/seriess", methods=["GET"]) @app.route("/seriess", methods=["GET"])
@login_required
def seriess(): def seriess():
seriess = Series.query.all() seriess = Series.query.all()
return render_template("seriess.html", seriess=seriess, message=st.GetMessage(), alert=st.GetAlert()) return render_template("seriess.html", seriess=seriess, message=st.GetMessage(), alert=st.GetAlert())
@@ -66,6 +68,7 @@ def seriess():
# /series -> GET/POST -> creates a new series type and when created, takes you back to /seriess # /series -> GET/POST -> creates a new series type and when created, takes you back to /seriess
################################################################################ ################################################################################
@app.route("/series", methods=["GET", "POST"]) @app.route("/series", methods=["GET", "POST"])
@login_required
def new_series(): def new_series():
form = SeriesForm(request.form) form = SeriesForm(request.form)
page_title='Create new Series' page_title='Create new Series'
@@ -87,6 +90,7 @@ def new_series():
# /series/<id> -> GET/POST(save or delete) -> shows/edits/delets a single series # /series/<id> -> GET/POST(save or delete) -> shows/edits/delets a single series
################################################################################ ################################################################################
@app.route("/series/<id>", methods=["GET", "POST"]) @app.route("/series/<id>", methods=["GET", "POST"])
@login_required
def series(id): def series(id):
### DDP: should this be request.form or request.values? ### DDP: should this be request.form or request.values?
form = SeriesForm(request.form) form = SeriesForm(request.form)
@@ -125,6 +129,7 @@ def series(id):
# /series/rating_reset -> forces a reset of calculated ratings of all series # /series/rating_reset -> forces a reset of calculated ratings of all series
################################################################################ ################################################################################
@app.route("/seriess/rating_reset", methods=["GET"]) @app.route("/seriess/rating_reset", methods=["GET"])
@login_required
def reset_all_series_ratings(): def reset_all_series_ratings():
s_list = Series.query.all() s_list = Series.query.all()
try: try:

View File

@@ -1,12 +1,16 @@
{% if not InDBox %} {% if not InDBox %}
<html> <!DOCTYPE html>
<html lang="en">
<head> <head>
<title>Book DB</title>
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Book DB">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous"> <link rel="stylesheet" href="{{ url_for( 'static', filename='upstream/bootstrap-5.0.2-dist/css/bootstrap.min.css' ) }}">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.18.0/dist/bootstrap-table.min.css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.18.0/dist/bootstrap-table.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.22/css/dataTables.bootstrap4.min.css"> <link rel="stylesheet" href="https://cdn.datatables.net/1.10.22/css/dataTables.bootstrap4.min.css">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
@@ -100,11 +104,19 @@
<a class="dropdown-item" href="{{url_for('reset_all_series_ratings')}}">Recalculate all series ratings</a> <a class="dropdown-item" href="{{url_for('reset_all_series_ratings')}}">Recalculate all series ratings</a>
</div class="dropdow-menu"> </div class="dropdow-menu">
</div class="nav-item dropdown"> </div class="nav-item dropdown">
</div clas="navbar-nav"> <form class="d-flex col ms-5" method="POST" action="/search">
<form class="form-inline my-2 my-lg-0" method="POST" action="/search">
<input class="form-control mr-sm-2" type="search" placeholder="Search by title" aria-label="Search" name="term"> <input class="form-control mr-sm-2" type="search" placeholder="Search by title" aria-label="Search" name="term">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form> </form>
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="UserMenu" role="button" data-toggle="dropdown" aria-expanded="false">
<svg width="20" height="20" fill="currentColor"><use xlink:href="{{url_for('static', filename='icons.svg')}}#user"/></svg>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="UserMenu">
<div><a class="dropdown-item" href="{{url_for('logout')}}">Logout</a></div>
</div class="dropdown-menu">
</div class="nav-item dropdown">
</div class="navbar-nav">
</div class="collapse navbar-collapse"> </div class="collapse navbar-collapse">
</nav> </nav>

66
templates/login.html Normal file
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Book DB Login</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Photo Assistant">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="{{ url_for( 'static', filename='upstream/bootstrap-5.0.2-dist/css/bootstrap.min.css' ) }}">
<!-- code to get bootstrap to work -->
<script src="{{ url_for( 'static', filename='upstream/jquery-3.6.0.min.js')}}"></script>
<script src="{{ url_for( 'static', filename='upstream/bootstrap-5.0.2-dist/js/bootstrap.min.js')}}"></script>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
{% import "bootstrap/wtf.html" as wtf %}
</head>
<body>
<div class="container">
{% if form.errors|length > 0 %}
<div class="row my-5">
<alert id="err" class="alert alert-danger alert-dismissible fade show">
<button type="button" class="close btn border-secondary me-3" data-dismiss="alert" aria-label="Close" onClick="$('#err').hide()">
<span aria-hidden="true">&times;</span>
</button>
{% set last_err = namespace(txt="") %}
{% for e in form.errors %}
{% if last_err.txt != form.errors[e] %}
{% set err = form.errors[e]|replace("['", "" ) %}
{% set err = err|replace("']", "" ) %}
{{err}}
{% set last_err.txt=form.errors[e] %}
{% endif %}
{% endfor %}
</alert>
</div class="row">
{% endif %}
<div class="row px-3 my-5 col-6" style="border: 3px solid #5bc0de; border-radius: 15px;">
<h3 class="my-3 text-center" style="color: #5bc0de">
<svg width="64" height="64" fill="currentColor"><use xlink:href="{{url_for('static', filename='book.svg')}}#logo" /></svg>&nbsp;Book DB Login</h3>
<form class="" method="POST">
<div class="input-group">
<label for="username" class="text-right input-group-text col-4 text-info">Username:</label>
<input class="form-control" type="text" id="username" name="username"></input>
</div>
<div class="input-group">
<label for="password" class="text-right input-group-text col-4 text-info">Password:</label>
<input class="form-control col-8" type="password" id="password" name="password"></input>
</div>
<div class="col-12 my-2 text-center">
{{ form.submit( class="form-control text-info") }}
</div>
{{ form.hidden_tag() }}
</form>
</div class="row">
</div class="container">
</body>
</html>

29
user.py Normal file
View File

@@ -0,0 +1,29 @@
from main import db
from sqlalchemy import Sequence
from flask_login import UserMixin, login_required
from main import db, app, ma
# pylint: disable=no-member
################################################################################
# Class describing Person in the database and DB via sqlalchemy
# id is unique id in DB
# dn is ldap distinguised name
# any entry in this DB is effectively a record you already authed successfully
# so acts as a session marker. If you fail ldap auth, you dont get a row here
################################################################################
class BDBUser(UserMixin,db.Model):
__tablename__ = "bdb_user"
id = db.Column(db.Integer, db.Sequence('bdb_user_id_seq'), primary_key=True)
dn = db.Column(db.String)
def __repr__(self):
str=f"<{self.__class__.__name__}("
for k, v in self.__dict__.items():
str += f"{k}={v!r}, "
str=str.rstrip(", ") + ")>"
return str
def get_id(self):
return self.dn