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