From c884260238667bba2cef5d0c920590825cffac56 Mon Sep 17 00:00:00 2001 From: augustin64 Date: Fri, 1 Nov 2024 21:55:45 +0100 Subject: [PATCH] This will need some cleaning in a near future --- isbn_sort/app.py | 29 +++--- isbn_sort/copy-book-data.py | 2 +- isbn_sort/init.sql | 4 + isbn_sort/modules/auth.py | 115 ++++++++++++++++++++++++ isbn_sort/{ => modules}/book.py | 47 +++++----- isbn_sort/{isbn_db.py => modules/db.py} | 42 +++------ isbn_sort/{ => modules}/sse.py | 0 isbn_sort/schema.sql | 14 ++- isbn_sort/static/dynamicUpdate.js | 11 ++- isbn_sort/static/main.js | 25 ++++-- isbn_sort/static/style.css | 37 +++++++- isbn_sort/templates/base.html | 6 ++ isbn_sort/templates/index.html | 39 ++++---- isbn_sort/templates/login.html | 12 +++ start.sh | 3 + 15 files changed, 278 insertions(+), 108 deletions(-) create mode 100644 isbn_sort/init.sql create mode 100644 isbn_sort/modules/auth.py rename isbn_sort/{ => modules}/book.py (71%) rename isbn_sort/{isbn_db.py => modules/db.py} (67%) rename isbn_sort/{ => modules}/sse.py (100%) create mode 100644 isbn_sort/templates/login.html mode change 100644 => 100755 start.sh diff --git a/isbn_sort/app.py b/isbn_sort/app.py index 8391e3d..c8769c7 100644 --- a/isbn_sort/app.py +++ b/isbn_sort/app.py @@ -2,11 +2,13 @@ from flask import render_template, redirect, request, flash, Blueprint, current_ import json -from .book import Book -from . import isbn_db -from . import sse +from .modules.book import Book +from .modules import db as isbn_db +from .modules import auth +from .modules import sse bp = Blueprint("app", __name__, url_prefix="/app") +bp.register_blueprint(auth.bp) announcer = sse.MessageAnnouncer() @@ -22,6 +24,7 @@ def announce_book(book, msg_type: str="add_book") -> None: @bp.route("/submit-isbn") +@auth.login_required def submit_isbn(): if "isbn" not in request.args: return "/submit-isbn?isbn=xxxxxx" @@ -38,7 +41,7 @@ def submit_isbn(): book = isbn_db.get_book(book.isbn) assert isbn_db.add_book(book) == "duplicate" # duplicate announce_book(isbn_db.get_book(book.isbn), msg_type="update_book") - return f"{book.title} ajouté (plusieurs occurrences)" + return f"{book.title} ajouté (doublon)" except IndexError: pass @@ -65,12 +68,14 @@ def submit_isbn(): @bp.route("/web-submit-isbn") +@auth.login_required def web_submit_isbn(): flash(submit_isbn()) return redirect(url_for("app.index")) @bp.route("/") +@auth.login_required def index(): return render_template( "index.html", @@ -80,6 +85,7 @@ def index(): @bp.route("/delete-book", methods=["POST"]) +@auth.login_required def delete_book(): if "isbn" not in request.form: return "missing isbn" @@ -96,8 +102,9 @@ def delete_book(): @bp.route("/update-book", methods=["POST"]) +@auth.login_required def update_book(): - attributes = ["isbn", "count", "title", "author", "publisher", "publish_date", "category"] + attributes = ["isbn", "title", "author", "status", "owner", "category"] if True in [i not in request.form for i in attributes]: return "missing an attribute" @@ -110,10 +117,9 @@ def update_book(): book = Book(form_data["isbn"]) book._manual_load( form_data["title"], - publisher=form_data["publisher"], - publish_date=form_data["publish_date"], + status=int(form_data["status"]), + owner=form_data["owner"], author=form_data["author"], - count=int(form_data["count"]), category=form_data["category"] ) isbn_db.update_book(book) @@ -122,13 +128,14 @@ def update_book(): @bp.route("/export-csv") +@auth.login_required def export_csv(): books = isbn_db.get_all_books() - csv = "ISBN;Titre;Auteur;Éditeur;Date;Catégorie;Quantité\n" + csv = "ISBN;Titre;Auteur;État;Propriétaire;Catégorie\n" for book in books: book.replace(";", ",") - csv += f"{book.isbn};{book.title};{book.author};{book.publisher};{book.publish_date};{book.category};{book.count}\n" + csv += f"{book.isbn};{book.title};{book.author};{book.status};{book.owner};{book.category}\n" # return as file with a good filename return current_app.response_class( @@ -139,6 +146,7 @@ def export_csv(): @bp.route('/listen', methods=['GET']) +@auth.login_required def listen(): def stream(): messages = announcer.listen() # returns a queue.Queue @@ -150,6 +158,7 @@ def listen(): @bp.route('/ping') +@auth.login_required def ping(): msg = sse.format_sse(data={"type": "pong"}) announcer.announce(msg=msg) diff --git a/isbn_sort/copy-book-data.py b/isbn_sort/copy-book-data.py index fc02b60..822fac2 100644 --- a/isbn_sort/copy-book-data.py +++ b/isbn_sort/copy-book-data.py @@ -6,7 +6,7 @@ import subprocess import sys import os -import book as bk +from modules import book as bk def clip_copy(data): diff --git a/isbn_sort/init.sql b/isbn_sort/init.sql new file mode 100644 index 0000000..071fc53 --- /dev/null +++ b/isbn_sort/init.sql @@ -0,0 +1,4 @@ +-- SQLite +-- Users +INSERT INTO user (username, password) +VALUES ('root', 'pbkdf2:sha256:260000$DzMbmkbgVJ0JoQa3$4b42c5a5135668ae5e5754fa4f0ac136ece8f59e8d751008bd533b6c9426c9ff'); \ No newline at end of file diff --git a/isbn_sort/modules/auth.py b/isbn_sort/modules/auth.py new file mode 100644 index 0000000..139135c --- /dev/null +++ b/isbn_sort/modules/auth.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 +""" +Authentification module +""" +import functools +from typing import Optional + +from flask import (Blueprint, flash, g, redirect, render_template, + request, session, url_for, current_app) + +from werkzeug.security import check_password_hash, generate_password_hash + +from .db import get_db + +bp = Blueprint("auth", __name__, url_prefix="/auth") + + +def login_required(view): + """View decorator that redirects anonymous users to the login page.""" + + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + flash("You need to login to access this resource.") + return redirect(url_for("app.auth.login")) + + return view(**kwargs) + + return wrapped_view + +def anon_required(view): + """View decorator that redirects authenticated users to the index.""" + + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is not None: + return redirect(url_for("app.index")) + + return view(**kwargs) + + return wrapped_view + + +@bp.before_app_request +def load_logged_in_user(): + """If a user id is stored in the session, load the user object from + the database into ``g.user``.""" + user_id = session.get("user_id") + + if user_id is None: + g.user = None + else: + g.user = ( + get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() + ) + + +def create_user(username: str, password: str) -> Optional[str]: + """Adds a new user to the database""" + error = None + if not username: + error = "Missing username." + elif not password: + error = "Missing password." + + try: + db = get_db() + + db.execute( + "INSERT INTO user (username, password) VALUES (?, ?)", + (username, generate_password_hash(password)), + ) + db.commit() + except db.IntegrityError: + # The username was already taken, which caused the + # commit to fail. Show a validation error. + error = f"Username {username} is not available." + + return error # may be None + + + +@bp.route("/login", methods=("GET", "POST")) +@anon_required +def login(): + """Log in a registered user by adding the user id to the session.""" + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + db = get_db() + error = None + user = db.execute( + "SELECT * FROM user WHERE username = ?", (username,) + ).fetchone() + + if (user is None) or not check_password_hash(user["password"], password): + error = "Incorrect username or password" + + if error is None: + # store the user id in a new session and return to the index + session.clear() + session["user_id"] = user["id"] + + return redirect(url_for("app.index")) + + flash(error) + + return render_template("login.html") + + +@bp.route("/logout") +def logout(): + """Clear the current session, including the stored user id.""" + session.clear() + return redirect("/") \ No newline at end of file diff --git a/isbn_sort/book.py b/isbn_sort/modules/book.py similarity index 71% rename from isbn_sort/book.py rename to isbn_sort/modules/book.py index a714953..db17b9f 100644 --- a/isbn_sort/book.py +++ b/isbn_sort/modules/book.py @@ -15,11 +15,19 @@ class Book: self.isbn = isbn self.title = None - self.publisher = None - self.publish_date = None + self.owner = None + self.status = 0 self.author = None self.category = None - self.count = -1 + + @property + def status_text(self) -> str: + return [ + "À lire", + "En cours", + "Lu" + ][self.status] + def _openlibrary_load(self, _): r = requests.get(f"https://openlibrary.org/api/books?bibkeys=ISBN:{self.isbn}&jscmd=details&format=json") @@ -35,15 +43,10 @@ class Book: isbn_data = data[f"ISBN:{self.isbn}"] self.title = isbn_data["details"]["title"] - if "publishers" in isbn_data["details"] and len(isbn_data["details"]["publishers"]) > 0: - self.publisher = isbn_data["details"]["publishers"][0] if "authors" in isbn_data["details"] and len(isbn_data["details"]["authors"]) > 0: self.author = isbn_data["details"]["authors"][0]["name"] - if "publish_date" in isbn_data["details"]: - self.publish_date = isbn_data["details"]["publish_date"] - def _google_books_load(self, config): if config["GOOGLE_BOOKS_KEY"] is None: @@ -65,23 +68,16 @@ class Book: self.title = item["volumeInfo"]["title"] - if "publisher" in item["volumeInfo"]: - self.publisher = item["volumeInfo"]["publisher"] - - if "publishedDate" in item["volumeInfo"]: - self.publish_date = item["volumeInfo"]["publishedDate"] - if "authors" in item["volumeInfo"]: self.author = item["volumeInfo"]["authors"][0] - def _manual_load(self, title, publisher=None, publish_date=None, author=None, count=-1, category=None): + def _manual_load(self, title, author=None, category=None, status=0, owner=None): self.title = title - self.publisher = publisher - self.publish_date = publish_date self.author = author - self.count = count self.category = category + self.status = status + self.owner = owner def load(self, config, loader="openlibrary"): if loader == "openlibrary": @@ -100,11 +96,11 @@ class Book: return { "isbn": self.isbn, "title": self.title, - "publisher": self.publisher, - "publish_date": self.publish_date, + "owner": self.owner, + "status": self.status, + "status_text": self.status_text, "author": self.author, - "category": self.category, - "count": self.count, + "category": self.category } def replace(self, pattern, replacement): @@ -116,8 +112,7 @@ class Book: self.isbn = rep(self.isbn) self.title = rep(self.title) - self.publisher = rep(self.publisher) - self.publish_date = rep(self.publish_date) + self.owner = rep(self.owner) + self.status = rep(self.status) self.author = rep(self.author) - self.category = rep(self.category) - self.count = rep(self.count) \ No newline at end of file + self.category = rep(self.category) \ No newline at end of file diff --git a/isbn_sort/isbn_db.py b/isbn_sort/modules/db.py similarity index 67% rename from isbn_sort/isbn_db.py rename to isbn_sort/modules/db.py index 516a816..2902abc 100644 --- a/isbn_sort/isbn_db.py +++ b/isbn_sort/modules/db.py @@ -33,10 +33,9 @@ def get_book(isbn): book = Book(isbn) book._manual_load( data["title"], - publisher=data["publisher"], - publish_date=data["publish_date"], author=data["author"], - count=data["count"], + status=data["status_"], + owner=data["owner_"], category=data["category"] if data["category"] != "" else None ) return book @@ -52,36 +51,18 @@ def delete_book(isbn): ) db.commit() -def increment_count(book): - if book.count == -1: - book = get_book(book.isbn) - - db = get_db() - db.execute( - """ - UPDATE book SET count=? - WHERE isbn=? - """, - (book.count+1, book.isbn) - ) - db.commit() - def add_book(book): try: book = get_book(book.isbn) - increment_count(book) return "duplicate" except IndexError: - if book.count == -1: - book.count = 1 - db = get_db() db.execute( """ - INSERT INTO book (isbn, count, title, author, publisher, publish_date, category) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO book (isbn, title, author, status_, owner_, category) + VALUES (?, ?, ?, ?, ?, ?) """, - (book.isbn, book.count, book.title, book.author, book.publisher, book.publish_date, book.category) + (book.isbn, book.title, book.author, book.status, book.owner, book.category) ) db.commit() return "added" @@ -90,10 +71,10 @@ def update_book(book): db = get_db() db.execute( """ - UPDATE book SET count=?, title=?, author=?, publisher=?, publish_date=?, category=? + UPDATE book SET title=?, author=?, status_=?, owner_=?, category=? WHERE isbn=? """, - (book.count, book.title, book.author, book.publisher, book.publish_date, book.category, book.isbn) + (book.title, book.author, book.status, book.owner, book.category, book.isbn) ) db.commit() return "updated" @@ -105,9 +86,7 @@ def get_all_books(): count += 1 if book.author is None: count += 1 - if book.publish_date is None: - count += 1 - if book.publisher is None: + if book.owner is None: count += 1 if book.category is None: count += 1 @@ -126,10 +105,9 @@ def get_all_books(): book = Book(data_row["isbn"]) book._manual_load( data_row["title"], - publisher=data_row["publisher"], - publish_date=data_row["publish_date"], + status=data_row["status_"], + owner=data_row["owner_"], author=data_row["author"], - count=data_row["count"], category=data_row["category"] if data_row["category"] != "" else None ) books.append(book) diff --git a/isbn_sort/sse.py b/isbn_sort/modules/sse.py similarity index 100% rename from isbn_sort/sse.py rename to isbn_sort/modules/sse.py diff --git a/isbn_sort/schema.sql b/isbn_sort/schema.sql index 985e746..647cb65 100644 --- a/isbn_sort/schema.sql +++ b/isbn_sort/schema.sql @@ -1,11 +1,17 @@ DROP TABLE IF EXISTS book; +DROP TABLE IF EXISTS user; CREATE TABLE book ( isbn INT PRIMARY KEY, - count INT DEFAULT 1, title TEXT, author TEXT, - publisher TEXT, - publish_date TEXT, - category TEXT DEFAULT '', + status_ INT DEFAULT 0, -- 0: A lire, 1: Commence, 2: Lu + owner_ TEXT DEFAULT '', + category TEXT DEFAULT '' +); + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL ); \ No newline at end of file diff --git a/isbn_sort/static/dynamicUpdate.js b/isbn_sort/static/dynamicUpdate.js index 96ea2d3..c1f7009 100644 --- a/isbn_sort/static/dynamicUpdate.js +++ b/isbn_sort/static/dynamicUpdate.js @@ -27,7 +27,11 @@ function viewAddBook(book) { } return '

'; } - let pc = (arg) => { if (arg > 1) return p("None"); else return p(arg); } + let ps = (arg) => { + if (arg == 0) return p("None"); + if (arg == 1) return '

' + else return '

'; + } let booksTableBody = document.getElementById("books-table").getElementsByTagName('tbody')[0]; @@ -37,10 +41,9 @@ function viewAddBook(book) { ''+book.isbn+''+ ''+p(book.title)+book.title+'

'+ ''+p(book.author)+book.author+'

'+ - ''+p(book.publish_date)+book.publish_date+'

'+ - ''+p(book.publisher)+book.publisher+'

'+ + ''+ps(book.status)+book.status_text+'

'+ + ''+p(book.owner)+book.owner+'

'+ ''+p(book.category)+book.category+'

'+ - ''+pc(book.count)+book.count+'

'+ ''+ ' '+ ' '+ diff --git a/isbn_sort/static/main.js b/isbn_sort/static/main.js index 4192507..98b2885 100644 --- a/isbn_sort/static/main.js +++ b/isbn_sort/static/main.js @@ -13,19 +13,17 @@ function getBookData(isbn) { // Extract data from the row var title = cells[1].innerText; var author = cells[2].innerText; - var date = cells[3].innerText; - var publisher = cells[4].innerText; + var status = {"À lire": 0, "En cours": 1, "Lu": 2}[cells[3].innerText]; + var owner = cells[4].innerText; var category = cells[5].innerText; - var quantity = cells[6].innerText; // Return the data return { title: title, author: author, - date: date, - publisher: publisher, + status: status, + owner: owner, category: category, - quantity: quantity }; } } @@ -41,10 +39,9 @@ function openEditBookDialog(isbn) { document.getElementById("edit-isbn").value = isbn; document.getElementById("edit-title").value = bookData.title; document.getElementById("edit-author").value = bookData.author; - document.getElementById("edit-date").value = bookData.date; - document.getElementById("edit-publisher").value = bookData.publisher; + document.getElementById("edit-owner").value = bookData.owner; + document.getElementById("edit-status").value = bookData.status; document.getElementById("edit-category").value = bookData.category; - document.getElementById("edit-quantity").value = bookData.quantity; editDialog.showModal(); } else { alert("Book not found!"); @@ -82,4 +79,14 @@ function openDeleteBookDialog(isbn) { function hideDeleteBookDialog() { var deleteDialog = document.getElementById("delete-book-dialog"); deleteDialog.close(); +} + +function categoryChange() { + edit_cat = document.getElementById("edit-category"); + if (edit_cat.selectedOptions[0].text == "- Nouvelle catégorie -") { + new_cat = window.prompt("Nom de la catégorie ?", ""); + if (new_cat == "") return; + + edit_cat.innerHTML += ''; + } } \ No newline at end of file diff --git a/isbn_sort/static/style.css b/isbn_sort/static/style.css index e85bb0a..c9605fd 100644 --- a/isbn_sort/static/style.css +++ b/isbn_sort/static/style.css @@ -4,7 +4,7 @@ /* Themes used: Catppuccin Latte & Moccha * https://github.com/catppuccin/catppuccin */ -/* Dark theme: Catpuccin Mocha +/* Dark theme: Catpuccin Mocha */ :root { --color-rosewater: #f5e0dc; --color-flamingo: #f2cdcd; @@ -33,10 +33,10 @@ --color-mantle: #181825; --color-crust: #11111b; --color-action: #333d5d; -}*/ +} /* Light theme: Catppuccin Latte */ -/*@media (prefers-color-scheme: light) {*/ +@media (prefers-color-scheme: light) { :root { --color-rosewater: #dc8a78; --color-flamingo: #dd7878; @@ -66,7 +66,7 @@ --color-crust: #dce0e8; --color-action: #cdd6f4; } -/*}*/ +} a { @@ -113,6 +113,7 @@ body { background-color: var(--color-base); color: var(--color-text); text-align: center; + margin: 0px; } #logout { @@ -236,6 +237,10 @@ code { color: var(--color-red); } +.green { + color: var(--color-green); +} + /** Style for table: alternate colors */ table { border-collapse: collapse; @@ -244,6 +249,7 @@ table { border-color: var(--color-crust); border-width: 2px; border-style: solid; + border-radius: 5px; } .action { @@ -295,3 +301,26 @@ table tr td:last-of-type { max-width: 100vw; overflow-x: scroll; } + +#container { + margin: 20px; +} + + +header { + position: sticky; + align-items: center; + justify-content: space-between; + display: flex; + + background-color: var(--color-surface0); + color: var(--color-text); + + top: 0; + padding: 20px; + margin-bottom: 20px; +} + +header > h1 { + margin: 0px; +} \ No newline at end of file diff --git a/isbn_sort/templates/base.html b/isbn_sort/templates/base.html index 1fc8975..508bd06 100644 --- a/isbn_sort/templates/base.html +++ b/isbn_sort/templates/base.html @@ -11,6 +11,12 @@ +
{% for message in get_flashed_messages() %}
{{ message }}
diff --git a/isbn_sort/templates/index.html b/isbn_sort/templates/index.html index c486c3e..ffeeefd 100644 --- a/isbn_sort/templates/index.html +++ b/isbn_sort/templates/index.html @@ -16,21 +16,24 @@


-
-
-
-
+
+
+
+
- + {% for category in categories %} {% if category != "" %} {% endif %} {% endfor %}
-
-
@@ -45,11 +48,13 @@
- Ajouter manuellement -
- - -
+
+ Ajouter manuellement +
+ + +
+
@@ -57,10 +62,9 @@ - - + + - {% for book in books %} @@ -68,10 +72,9 @@ - - + + -
ISBN Titre AuteurDateÉditeurÉtatPropriétaire CatégorieQuantité Actions
{{ book.isbn }}

{{ book.title }}

{{ book.author }}

{{ book.publish_date }}

{{ book.publisher }}

{{ book.status_text }}

{{ book.owner }}

{{ book.category }}

{{ book.count }}

diff --git a/isbn_sort/templates/login.html b/isbn_sort/templates/login.html new file mode 100644 index 0000000..095d6d6 --- /dev/null +++ b/isbn_sort/templates/login.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + + +{% block content %} +

{% block title %}Log in{% endblock %}

+ +
+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index eac4353..82b07a0 --- a/start.sh +++ b/start.sh @@ -1 +1,4 @@ +#!/bin/bash + +# FLASK_ENV=development flask run --port=5000 --debug flask run --host=0.0.0.0 --port=5000 \ No newline at end of file