From 278a94d13a19efe5416cc9c57e230dd2495edb38 Mon Sep 17 00:00:00 2001 From: Hansyel-droid Date: Wed, 28 May 2025 23:49:22 +0800 Subject: [PATCH 1/2] Add REST API for notes with full CRUD and related updates --- project/app.py | 64 +++++++++++++++++++++++++++------------ project/create_tables.py | 5 +++ project/flaskr.db | Bin 8192 -> 12288 bytes project/models.py | 11 +++++++ 4 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 project/create_tables.py diff --git a/project/app.py b/project/app.py index 5b9dd77..067e92b 100644 --- a/project/app.py +++ b/project/app.py @@ -15,7 +15,6 @@ ) from flask_sqlalchemy import SQLAlchemy - basedir = Path(__file__).resolve().parent # configuration @@ -31,17 +30,13 @@ SQLALCHEMY_DATABASE_URI = url SQLALCHEMY_TRACK_MODIFICATIONS = False - # create and initialize a new Flask app app = Flask(__name__) -# load the config app.config.from_object(__name__) -# init sqlalchemy db = SQLAlchemy(app) from project import models - def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): @@ -49,20 +44,15 @@ def decorated_function(*args, **kwargs): flash("Please log in.") return jsonify({"status": 0, "message": "Please log in."}), 401 return f(*args, **kwargs) - return decorated_function - @app.route("/") def index(): - """Searches the database for entries, then displays them.""" entries = db.session.query(models.Post) return render_template("index.html", entries=entries) - @app.route("/add", methods=["POST"]) def add_entry(): - """Adds new post to the database.""" if not session.get("logged_in"): abort(401) new_entry = models.Post(request.form["title"], request.form["text"]) @@ -71,10 +61,8 @@ def add_entry(): flash("New entry was successfully posted") return redirect(url_for("index")) - @app.route("/login", methods=["GET", "POST"]) def login(): - """User login/authentication/session management.""" error = None if request.method == "POST": if request.form["username"] != app.config["USERNAME"]: @@ -87,23 +75,18 @@ def login(): return redirect(url_for("index")) return render_template("login.html", error=error) - @app.route("/logout") def logout(): - """User logout/authentication/session management.""" session.pop("logged_in", None) flash("You were logged out") return redirect(url_for("index")) - @app.route("/delete/", methods=["GET"]) @login_required def delete_entry(post_id): - """Deletes post from database.""" result = {"status": 0, "message": "Error"} try: - new_id = post_id - db.session.query(models.Post).filter_by(id=new_id).delete() + db.session.query(models.Post).filter_by(id=post_id).delete() db.session.commit() result = {"status": 1, "message": "Post Deleted"} flash("The entry was deleted.") @@ -111,7 +94,6 @@ def delete_entry(post_id): result = {"status": 0, "message": repr(e)} return jsonify(result) - @app.route("/search/", methods=["GET"]) def search(): query = request.args.get("query") @@ -120,6 +102,50 @@ def search(): return render_template("search.html", entries=entries, query=query) return render_template("search.html") +# === REST API for Notes === + +@app.route("/api/notes", methods=["GET"]) +def get_notes(): + notes = db.session.query(models.Note).all() + return jsonify([note.to_dict() for note in notes]), 200 + +@app.route("/api/notes/", methods=["GET"]) +def get_note(note_id): + note = db.session.get(models.Note, note_id) + if note: + return jsonify(note.to_dict()), 200 + return jsonify({"error": "Note not found"}), 404 + +@app.route("/api/notes", methods=["POST"]) +def create_note(): + data = request.get_json() + if not data or "content" not in data: + return jsonify({"error": "Content is required"}), 400 + note = models.Note(content=data["content"]) + db.session.add(note) + db.session.commit() + return jsonify(note.to_dict()), 201 + +@app.route("/api/notes/", methods=["PUT"]) +def update_note(note_id): + note = db.session.get(models.Note, note_id) + if not note: + return jsonify({"error": "Note not found"}), 404 + data = request.get_json() + if not data or "content" not in data: + return jsonify({"error": "Content is required"}), 400 + note.content = data["content"] + db.session.commit() + return jsonify(note.to_dict()), 200 + +@app.route("/api/notes/", methods=["DELETE"]) +def delete_note(note_id): + note = db.session.get(models.Note, note_id) + if not note: + return jsonify({"error": "Note not found"}), 404 + db.session.delete(note) + db.session.commit() + return jsonify({"message": "Note deleted"}), 200 if __name__ == "__main__": app.run() diff --git a/project/create_tables.py b/project/create_tables.py new file mode 100644 index 0000000..236b8a6 --- /dev/null +++ b/project/create_tables.py @@ -0,0 +1,5 @@ +from app import app, db + +with app.app_context(): + db.create_all() + print("Tables created successfully.") diff --git a/project/flaskr.db b/project/flaskr.db index 730d1238b1b77f3ffbc0f5a8620ece794b67b8c5..7a7a5f689f379d1fc5c1d5ac9cc510c95b0c4907 100644 GIT binary patch delta 213 zcmZp0Xh@hKEhx>vz`zW|Fu*ra#~3K6SJur76k_6MW8g36XWLjfjZY(=iCtV&l(EsV zBrz!`H7~yejG3K-TpdGP6+#@Hd|VYkqLUZ!OS32E=K z|8t;`n;H1e@o(neyjf6SDZj5IGc$u_NNRD30#u7aT2X$kLUL(QjyMxDgK0=cX0ZYg Nl^`hrx=a$Q1OUobJ5>Mx delta 53 zcmZojXmFSyEhxsoz`z8=Fu*%e$CzJ?K`&mG7bwKYU(Uc^zFAO!lYetLf2RNd$Xf^* diff --git a/project/models.py b/project/models.py index 8434fa1..2e1fc7a 100644 --- a/project/models.py +++ b/project/models.py @@ -12,3 +12,14 @@ def __init__(self, title, text): def __repr__(self): return f"" + + +class Note(db.Model): + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.String, nullable=False) + + def __init__(self, content): + self.content = content + + def to_dict(self): + return {"id": self.id, "content": self.content} From 37d75fe110f3190490bf2c173ba24427d39f174a Mon Sep 17 00:00:00 2001 From: Hansyel-droid <hansmagalaman123@gmail.com> Date: Fri, 30 May 2025 14:18:34 +0800 Subject: [PATCH 2/2] Add tests for missing coverage and fix JSON error handling --- .coverage | Bin 0 -> 53248 bytes flaskr.db | Bin 0 -> 12288 bytes project/__init__.py | 27 ++++++++++++ project/app.py | 78 +++++++++++++++------------------ project/flaskr.db | Bin 12288 -> 12288 bytes project/init_db.py | 6 +++ project/models.py | 6 +-- project/test_api.py | 59 +++++++++++++++++++++++++ project/test_app_api_errors.py | 70 +++++++++++++++++++++++++++++ 9 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 .coverage create mode 100644 flaskr.db create mode 100644 project/init_db.py create mode 100644 project/test_api.py create mode 100644 project/test_app_api_errors.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..b72040c7316ff05c95585ee80be9f755cc710013 GIT binary patch literal 53248 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 literal 0 HcmV?d00001 diff --git a/flaskr.db b/flaskr.db new file mode 100644 index 0000000000000000000000000000000000000000..018dcf346555f51f9ed43c600bb5815bc6af3dec GIT binary patch literal 12288 zcmeI%K}*9h7zW@ktxQ_fj=>af-*E%+;vX=MpkuXat-_AgRVZ}Ja6!H9>|gT7Et&Q( zaN=c%@CLp%P13yOxs}}B#HA_tNk5Ns!za`vA?TEu$ZwIoe3eJ!ec1k%t-6W2v;J=P zIDUEGbqktA(QDLLzZNqPfB*y_009U<00Izz00bZaflUi&Z~LI#7ISIxhpL!pvn;=t zDw!$H<XNoPjdCXp$`SXIOr5KgldFuAyEs1LFnH7x`<CH*nf5N^>fCkOzm(~a2WrTj za?}mG^M?3L)XT)Fo*7r(N}?P2A<>kkD!$D##oKQ`r2aSGv@rIE00bZa0SG_<0uX=z z1Rwwb2teS^1>9M_J>y4%00bZa0SG_<0uX=z1Rwwb2teRJ3+xM@TEdU^#E$R-`*1kR I$J44<01re@cK`qY literal 0 HcmV?d00001 diff --git a/project/__init__.py b/project/__init__.py index e69de29..04c2371 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -0,0 +1,27 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +import os +from pathlib import Path + +basedir = Path(__file__).resolve().parent.parent + +app = Flask(__name__) + +# Basic Config +app.config['SECRET_KEY'] = 'change_me' +app.config['USERNAME'] = 'admin' +app.config['PASSWORD'] = 'admin' + +# Database Config +DATABASE = "flaskr.db" +url = os.getenv("DATABASE_URL", f"sqlite:///{basedir / DATABASE}") +if url.startswith("postgres://"): + url = url.replace("postgres://", "postgresql://", 1) +app.config['SQLALCHEMY_DATABASE_URI'] = url +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +# Initialize DB +db = SQLAlchemy(app) + +# Register models +from project import models diff --git a/project/app.py b/project/app.py index 067e92b..536f6e9 100644 --- a/project/app.py +++ b/project/app.py @@ -1,42 +1,12 @@ -import os -from functools import wraps -from pathlib import Path - from flask import ( - Flask, - render_template, - request, - session, - flash, - redirect, - url_for, - abort, - jsonify, + render_template, request, session, flash, + redirect, url_for, abort, jsonify ) -from flask_sqlalchemy import SQLAlchemy - -basedir = Path(__file__).resolve().parent - -# configuration -DATABASE = "flaskr.db" -USERNAME = "admin" -PASSWORD = "admin" -SECRET_KEY = "change_me" -url = os.getenv("DATABASE_URL", f"sqlite:///{Path(basedir).joinpath(DATABASE)}") - -if url.startswith("postgres://"): - url = url.replace("postgres://", "postgresql://", 1) - -SQLALCHEMY_DATABASE_URI = url -SQLALCHEMY_TRACK_MODIFICATIONS = False - -# create and initialize a new Flask app -app = Flask(__name__) -app.config.from_object(__name__) -db = SQLAlchemy(app) - +from functools import wraps +from project import app, db from project import models + def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): @@ -46,11 +16,13 @@ def decorated_function(*args, **kwargs): return f(*args, **kwargs) return decorated_function + @app.route("/") def index(): - entries = db.session.query(models.Post) + entries = db.session.query(models.Post).all() return render_template("index.html", entries=entries) + @app.route("/add", methods=["POST"]) def add_entry(): if not session.get("logged_in"): @@ -61,13 +33,14 @@ def add_entry(): flash("New entry was successfully posted") return redirect(url_for("index")) + @app.route("/login", methods=["GET", "POST"]) def login(): error = None if request.method == "POST": - if request.form["username"] != app.config["USERNAME"]: + if request.form["username"] != "admin": # or get from config error = "Invalid username" - elif request.form["password"] != app.config["PASSWORD"]: + elif request.form["password"] != "admin": # or get from config error = "Invalid password" else: session["logged_in"] = True @@ -75,12 +48,14 @@ def login(): return redirect(url_for("index")) return render_template("login.html", error=error) + @app.route("/logout") def logout(): session.pop("logged_in", None) flash("You were logged out") return redirect(url_for("index")) + @app.route("/delete/<int:post_id>", methods=["GET"]) @login_required def delete_entry(post_id): @@ -94,14 +69,17 @@ def delete_entry(post_id): result = {"status": 0, "message": repr(e)} return jsonify(result) + @app.route("/search/", methods=["GET"]) def search(): query = request.args.get("query") entries = db.session.query(models.Post) if query: - return render_template("search.html", entries=entries, query=query) + entries = entries.filter(models.Post.title.contains(query)) + return render_template("search.html", entries=entries.all(), query=query) return render_template("search.html") + # === REST API for Notes === @app.route("/api/notes", methods=["GET"]) @@ -109,6 +87,7 @@ def get_notes(): notes = db.session.query(models.Note).all() return jsonify([note.to_dict() for note in notes]), 200 + @app.route("/api/notes/<int:note_id>", methods=["GET"]) def get_note(note_id): note = db.session.get(models.Note, note_id) @@ -116,28 +95,42 @@ def get_note(note_id): return jsonify(note.to_dict()), 200 return jsonify({"error": "Note not found"}), 404 + @app.route("/api/notes", methods=["POST"]) def create_note(): - data = request.get_json() + try: + data = request.get_json(force=True) + except Exception: + return jsonify({"error": "Invalid JSON"}), 400 + if not data or "content" not in data: return jsonify({"error": "Content is required"}), 400 + note = models.Note(content=data["content"]) db.session.add(note) db.session.commit() return jsonify(note.to_dict()), 201 + @app.route("/api/notes/<int:note_id>", methods=["PUT"]) def update_note(note_id): note = db.session.get(models.Note, note_id) if not note: return jsonify({"error": "Note not found"}), 404 - data = request.get_json() + + try: + data = request.get_json(force=True) + except Exception: + return jsonify({"error": "Invalid JSON"}), 400 + if not data or "content" not in data: return jsonify({"error": "Content is required"}), 400 + note.content = data["content"] db.session.commit() return jsonify(note.to_dict()), 200 + @app.route("/api/notes/<int:note_id>", methods=["DELETE"]) def delete_note(note_id): note = db.session.get(models.Note, note_id) @@ -147,5 +140,6 @@ def delete_note(note_id): db.session.commit() return jsonify({"message": "Note deleted"}), 200 + if __name__ == "__main__": - app.run() + app.run(debug=True) diff --git a/project/flaskr.db b/project/flaskr.db index 7a7a5f689f379d1fc5c1d5ac9cc510c95b0c4907..749416506ea95652b01d449652de27bb0c937468 100644 GIT binary patch delta 50 zcmZojXh@hK%_uWb#+gxOW5N=CHb(w04E$d<3kp2spBTW-C&<9X$ig5h=#iLL?2)-h GK>z@7i4AlB delta 50 ycmZojXh@hK%_u!l#+gxiW5N=CHU<F(2LA7x1qB}RPYht^6J&q@PB67dK>z?lhzc42 diff --git a/project/init_db.py b/project/init_db.py new file mode 100644 index 0000000..44a5536 --- /dev/null +++ b/project/init_db.py @@ -0,0 +1,6 @@ +from project import create_app, db + +app = create_app() + +with app.app_context(): + db.create_all() diff --git a/project/models.py b/project/models.py index 2e1fc7a..5e39f0c 100644 --- a/project/models.py +++ b/project/models.py @@ -1,5 +1,4 @@ -from project.app import db - +from project import db class Post(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -11,8 +10,7 @@ def __init__(self, title, text): self.text = text def __repr__(self): - return f"<title {self.title}>" - + return f"<Post {self.title}>" class Note(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/project/test_api.py b/project/test_api.py new file mode 100644 index 0000000..42d3c12 --- /dev/null +++ b/project/test_api.py @@ -0,0 +1,59 @@ +import pytest +import json +from project import app, db, models + +@pytest.fixture +def client(): + app.config["TESTING"] = True + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + with app.test_client() as client: + with app.app_context(): + db.create_all() + yield client + with app.app_context(): + db.drop_all() + +def test_create_note_success(client): + response = client.post("/api/notes", json={"content": "Test note"}) + assert response.status_code == 201 + data = response.get_json() + assert data["content"] == "Test note" + assert "id" in data + +def test_create_note_no_content(client): + response = client.post("/api/notes", json={}) + assert response.status_code == 400 + +def test_get_notes_empty(client): + response = client.get("/api/notes") + assert response.status_code == 200 + data = response.get_json() + assert data == [] + +def test_get_note_not_found(client): + response = client.get("/api/notes/999") + assert response.status_code == 404 + +def test_update_note_success(client): + # Create a note first + client.post("/api/notes", json={"content": "Old content"}) + response = client.put("/api/notes/1", json={"content": "New content"}) + assert response.status_code == 200 + data = response.get_json() + assert data["content"] == "New content" + +def test_update_note_not_found(client): + response = client.put("/api/notes/999", json={"content": "Test"}) + assert response.status_code == 404 + +def test_delete_note_success(client): + # Create a note first + client.post("/api/notes", json={"content": "To delete"}) + response = client.delete("/api/notes/1") + assert response.status_code == 200 + data = response.get_json() + assert "deleted" in data["message"].lower() + +def test_delete_note_not_found(client): + response = client.delete("/api/notes/999") + assert response.status_code == 404 diff --git a/project/test_app_api_errors.py b/project/test_app_api_errors.py new file mode 100644 index 0000000..fdcf5da --- /dev/null +++ b/project/test_app_api_errors.py @@ -0,0 +1,70 @@ +import json +from project import app, db, models + + +def setup_module(module): + """Set up test client and clean DB.""" + app.config["TESTING"] = True + app.config["WTF_CSRF_ENABLED"] = False + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + with app.app_context(): + db.create_all() + module.client = app.test_client() + + +def teardown_module(module): + with app.app_context(): + db.drop_all() + + +def test_search_without_query(): + """Covers: search view without query (uncovered path).""" + response = client.get("/search/") + assert response.status_code == 200 + assert b"Search" in response.data # assuming template title + + +def test_login_invalid_username(): + """Covers: login route with invalid username.""" + response = client.post("/login", data={ + "username": "wrong", + "password": "admin" + }, follow_redirects=True) + assert b"Invalid username" in response.data + + +def test_login_invalid_password(): + """Covers: login route with invalid password.""" + response = client.post("/login", data={ + "username": "admin", + "password": "wrong" + }, follow_redirects=True) + assert b"Invalid password" in response.data + + +def test_create_note_missing_json(): + """Covers: missing JSON in create_note.""" + response = client.post("/api/notes", data="not-json", content_type="application/json") + assert response.status_code == 400 + assert response.json["error"] == "Invalid JSON" + + +def test_update_note_missing_json(): + """Covers: missing JSON in update_note.""" + # Add a note first + with app.app_context(): + note = models.Note(content="sample") + db.session.add(note) + db.session.commit() + note_id = note.id + + response = client.put(f"/api/notes/{note_id}", data="not-json", content_type="application/json") + assert response.status_code == 400 + assert response.json["error"] == "Invalid JSON" + + +def test_delete_note_not_found(): + """Covers: trying to delete a nonexistent note.""" + response = client.delete("/api/notes/9999") + assert response.status_code == 404 + assert response.json["error"] == "Note not found"