diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..b72040c Binary files /dev/null and b/.coverage differ diff --git a/flaskr.db b/flaskr.db new file mode 100644 index 0000000..018dcf3 Binary files /dev/null and b/flaskr.db differ 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 5b9dd77..536f6e9 100644 --- a/project/app.py +++ b/project/app.py @@ -1,44 +1,9 @@ -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__) -# load the config -app.config.from_object(__name__) -# init sqlalchemy -db = SQLAlchemy(app) - +from functools import wraps +from project import app, db from project import models @@ -49,20 +14,17 @@ 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) + entries = db.session.query(models.Post).all() 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"]) @@ -74,12 +36,11 @@ def add_entry(): @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"]: + 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 @@ -90,7 +51,6 @@ def login(): @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")) @@ -99,11 +59,9 @@ def logout(): @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.") @@ -117,9 +75,71 @@ 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"]) +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(): + 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/", 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 + + 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/", 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() + app.run(debug=True) 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 730d123..7494165 100644 Binary files a/project/flaskr.db and b/project/flaskr.db differ 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 8434fa1..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,4 +10,14 @@ def __init__(self, title, text): self.text = text def __repr__(self): - return f"" + return f"<Post {self.title}>" + +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} 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"