From ca7a3303be287217706a88888d146aeca20b245f Mon Sep 17 00:00:00 2001 From: Arun J <cb.en.u4cce19010@cb.students.amrita.edu> Date: Sat, 26 Dec 2020 17:36:41 +0530 Subject: [PATCH] Display book records and implement edit/delete Other changes: * create_db.py also generates dummy data * Readme updated with installation instructions * Production version added (run_prod.py) * Debug version uses livereload for automatic reload of pages * AddBookForm generalized so that it can also be used for /edit * Fixed relationship between User and Book table * Added is_admin method to check if current user is an admin --- README.md | 30 ++++++++++++++++++++++- bam/forms.py | 6 ++--- bam/models.py | 8 +++--- bam/routes.py | 38 ++++++++++++++++++++++++---- bam/static/hometable.css | 35 ++++++++++++++++++++++++++ bam/templates/books.html | 41 +++++++++++++++++++++++++++++-- bam/templates/edit.html | 26 ++++++++++++++++++++ create_db.py | 53 +++++++++++++++++++++++++++++----------- requirements.txt | 1 + run.py | 11 ++++++++- run_prod.py | 4 +++ 11 files changed, 224 insertions(+), 29 deletions(-) create mode 100644 bam/static/hometable.css create mode 100644 bam/templates/edit.html create mode 100644 run_prod.py diff --git a/README.md b/README.md index 8934a3b..7d89294 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ # BAM -A Book Management System built with Flask \ No newline at end of file +> A non-traditional Netflix-inspired Book Management System built with Flask + +## Demo + +A demo is available at https://bam.hackerdash.online - to try it out, you can use `bookmaster` as username and `masterofbooks` for the password. + +## Running locally + +```zsh +# Clone this repo +git clone https://git.amrita.edu/a/bam.git +cd bam + +# Create virtualenv +python -m venv bam-venv +. ./bam-venv/bin/activate + +# Install dependencies and initialize tables +pip install -r requirements.txt +python create_db.py + +# Start the development server on http://localhost:5000 +python run.py + +# Or alternatively, run the production server +sudo python run_prod.py +``` + +Tested on Manjaro Linux / Python 3.8.6 diff --git a/bam/forms.py b/bam/forms.py index c2b40c5..04d1925 100644 --- a/bam/forms.py +++ b/bam/forms.py @@ -63,17 +63,17 @@ class LoginForm(FlaskForm): submit = SubmitField("Login") -class AddBookForm(FlaskForm): +class BookForm(FlaskForm): title = StringField("Title", validators=[DataRequired()]) author = StringField("Author", validators=[DataRequired()]) isbn = StringField( "ISBN", validators=[ Regexp( - r"^(\d{10}\d{3}?)?$", message="Invalid ISBN. Must be 10 or 13 digits." + r"^(\d{10}(\d{3})?)?$", message="Invalid ISBN. Must be 10 or 13 digits." ), ], ) price = DecimalField("Price", default=0.0) - submit = SubmitField("Add book") + submit = SubmitField("Submit") diff --git a/bam/models.py b/bam/models.py index 76faa81..460bc16 100644 --- a/bam/models.py +++ b/bam/models.py @@ -13,10 +13,14 @@ class User(db.Model, UserMixin): username = db.Column(db.String(16), unique=True, nullable=False) password = db.Column(db.String(60), nullable=False) role = db.Column(db.String(10), default="user") + books = db.relationship("Book", backref="addedby", lazy=True) def __repr__(self): return f"User({self.email}, {self.password})" + def is_admin(self): + return self.role == "admin" + class Book(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -24,6 +28,4 @@ class Book(db.Model): author = db.Column(db.String(120), nullable=False) isbn = db.Column(db.String(13)) price = db.Column(db.Float, default=0.0) - addedby = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) - - users = db.relationship(User) \ No newline at end of file + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) diff --git a/bam/routes.py b/bam/routes.py index c6fbb02..7c301b2 100644 --- a/bam/routes.py +++ b/bam/routes.py @@ -1,7 +1,7 @@ from flask import render_template, url_for, flash, redirect from flask_login import login_user, current_user, logout_user, login_required from bam import app, db, bcrypt -from bam.forms import RegistrationForm, LoginForm, AddBookForm +from bam.forms import RegistrationForm, LoginForm, BookForm from bam.models import User, Book @@ -9,7 +9,7 @@ from bam.models import User, Book @app.route("/home") def home(): if current_user.is_authenticated: - return render_template("books.html") + return render_template("books.html", books=Book.query.all()) return render_template("home.html") @@ -56,16 +56,44 @@ def logout(): @app.route("/add", methods=["GET", "POST"]) @login_required def addBook(): - form = AddBookForm() + form = BookForm() if form.validate_on_submit(): book = Book( title=form.title.data, author=form.author.data, isbn=form.isbn.data, price=form.price.data, - addedby=current_user.id, + user_id=current_user.id, ) db.session.add(book) db.session.commit() return redirect(url_for("home")) - return render_template("add.html", title="Add book", form=form) \ No newline at end of file + return render_template("add.html", title="Add book", form=form) + + +@app.route("/edit/<int:bookid>", methods=["GET", "POST"]) +@login_required +def editBook(bookid): + book = Book.query.get(bookid) + if book and (current_user.is_admin() or current_user.id == book.addedby.id): + form = BookForm() + if not form.validate_on_submit(): + return render_template("edit.html", form=form, book=book) + + book.title = form.title.data + book.author = form.author.data + book.isbn = form.isbn.data + book.price = form.price.data + db.session.commit() + + return redirect(url_for("home")) + + +@app.route("/delete/<int:bookid>") +@login_required +def deleteBook(bookid): + book = Book.query.get(bookid) + if book and (current_user.is_admin() or current_user.id == book.addedby.id): + db.session.delete(book) + db.session.commit() + return redirect(url_for("home")) diff --git a/bam/static/hometable.css b/bam/static/hometable.css new file mode 100644 index 0000000..f4d1421 --- /dev/null +++ b/bam/static/hometable.css @@ -0,0 +1,35 @@ +table { + width: 80vw; + font-size: 18px; + border-collapse: collapse; + color: #eee; + --pad-th: 11px; + --pad-td: 7px; +} + +table a { + color: #ccc; + text-decoration: underline 1px dotted grey; +} + +thead tr { + border-bottom: 1px solid #666; +} + +th { + background-color: #171717; + text-align: left; + padding: var(--pad-th); +} + +td { + padding: var(--pad-th); +} + +tr:nth-child(even) td { + background-color: #1b1b1b; +} + +tr:nth-child(odd) td { + background-color: #1f1f1f; +} diff --git a/bam/templates/books.html b/bam/templates/books.html index b860055..0d571ea 100644 --- a/bam/templates/books.html +++ b/bam/templates/books.html @@ -1,3 +1,40 @@ -{% extends "dash.html" %} {% set active_page = "books" %} {% block content %} -<div>WIP</div> +{% extends "dash.html" %} {% set active_page = "books" %} {% block head %} +<link + rel="stylesheet" + type="text/css" + href="{{ url_for('static', filename='hometable.css') }}" +/> +{% endblock %} {% block content %} +<h4>Welcome {{ current_user.username }}. Find the book list below:</h4> +<table id="bookTable"> + <thead> + <tr> + <th>Book Title</th> + <th>Author</th> + <th>ISBN</th> + <th>Price</th> + <th>Added by</th> + <th>Edit</th> + <th>Delete</th> + </tr> + </thead> + <tbody> + {% for book in books %} + <tr> + <td>{{ book.title }}</td> + <td>{{ book.author }}</td> + <td>{{ book.isbn }}</td> + <td>Rs {{ book.price }}</td> + <td>{{ book.addedby.username }}</td> + {% if current_user.is_admin() or current_user.id == book.addedby.id %} + <td><a href="{{ url_for('editBook', bookid=book.id) }}">Edit</a></td> + <td><a href="{{ url_for('deleteBook', bookid=book.id) }}">Delete</a></td> + {% else %} + <td>-</td> + <td>-</td> + {% endif %} + </tr> + {% endfor %} + </tbody> +</table> {% endblock %} diff --git a/bam/templates/edit.html b/bam/templates/edit.html new file mode 100644 index 0000000..cda55a4 --- /dev/null +++ b/bam/templates/edit.html @@ -0,0 +1,26 @@ +{% extends "dash.html" %} {% set active_page = "add" %} {% block head %} +<link + rel="stylesheet" + href="{{ url_for('static', filename='bookform.css') }}" +/> +{% endblock %} {% block content %} +<form class="addBookForm" action="" method="POST"> + {{ form.hidden_tag() }} + + <h1>Edit {{ book.title|e }}</h1> + {{ form.title.label() }} {{ form.title(value=book.title) }} {% for error in + form.title.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.author.label() }} {{ form.author(value=book.author) }} {% + for error in form.author.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.isbn.label() }} {{ form.isbn(value=book.isbn) if + book.isbn else form.isbn }} {% for error in form.isbn.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.price.label() }} {{ form.price(value=book.price) }} {% + for error in form.price.errors %} + <div class="field-error">{{ error }}</div> + {% endfor %} {{ form.submit() }} +</form> + +{% endblock %} diff --git a/create_db.py b/create_db.py index 3f5bffe..58a9a34 100644 --- a/create_db.py +++ b/create_db.py @@ -1,15 +1,40 @@ from bam import db, bcrypt -from bam.models import User - -# Create databases -db.create_all() - -# Create a test user -db.session.add( - User( - username="bookmaster", - email="bookmaster@example.com", - password=bcrypt.generate_password_hash("masterofbooks").decode("utf-8"), - ) -) -db.session.commit() \ No newline at end of file +from bam.models import User, Book + + +def main(): + # Create databases + db.create_all() + + if input("Populate db with test data? [Y/n] ").lower == "n": + return + + users = [ + ["bookmaster", "bookmaster@example.com", "masterofbooks", "user"], + ["root", "root@example.com", "toor", "admin"], + ] + + books = [ + ["Harry Potter", "JK Rowling", None, "400", 1], + ["Lord of the Rings", "JRR Tolkien", None, "700.50", 2], + ["Artemis Fowl", "Eoin Colfer", None, "356", 1], + ] + + for username, email, passwd, role in users: + db.session.add( + User( + username=username, + email=email, + password=bcrypt.generate_password_hash(passwd).decode("utf-8"), + role=role, + ) + ) + for title, author, isbn, price, user_id in books: + db.session.add( + Book(title=title, author=author, isbn=isbn, price=price, user_id=user_id) + ) + db.session.commit() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1752fdb..2b9a573 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,5 +23,6 @@ SQLAlchemy==1.3.22 toml==0.10.2 typed-ast==1.4.1 typing-extensions==3.7.4.3 +waitress==1.4.4 Werkzeug==1.0.1 WTForms==2.3.3 diff --git a/run.py b/run.py index 83f079b..4c31e8b 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,13 @@ from bam import app +from livereload import Server + + +def main(): + app.debug = True + + server = Server(app.wsgi_app) + server.serve(port=5000) + if __name__ == "__main__": - app.run(debug=True) + main() \ No newline at end of file diff --git a/run_prod.py b/run_prod.py new file mode 100644 index 0000000..3732b4d --- /dev/null +++ b/run_prod.py @@ -0,0 +1,4 @@ +from bam import app +from waitress import serve + +serve(app, host="0.0.0.0", port=80) -- GitLab