diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..74923076 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,25 @@ +from flask import Flask +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +engine = create_engine('sqlite:///database.db', echo=True) +db_session = scoped_session(sessionmaker(bind=engine)) + +Base = declarative_base() +Base.query = db_session.query_property() + +def create_app(): + app = Flask(__name__) + + from .routes import notes_bp, tags_bp, comments_bp + app.register_blueprint(notes_bp) + app.register_blueprint(tags_bp) + app.register_blueprint(comments_bp) + + @app.teardown_appcontext + def shutdown_session(exception=None): + db_session.remove() + + Base.metadata.create_all(bind=engine) + return app diff --git a/app/models.py b/app/models.py new file mode 100644 index 00000000..7d70f6e9 --- /dev/null +++ b/app/models.py @@ -0,0 +1,31 @@ +from . import Base +from sqlalchemy import Column, Integer, String, Text, Table, ForeignKey, DateTime +from sqlalchemy.orm import relationship +import datetime + +note_tag = Table( + 'note_tag', Base.metadata, + Column('note_id', Integer, ForeignKey('notes.id')), + Column('tag_id', Integer, ForeignKey('tags.id')) +) + +class Note(Base): + __tablename__ = 'notes' + id = Column(Integer, primary_key=True) + title = Column(String(100)) + content = Column(Text) + tags = relationship('Tag', secondary=note_tag, back_populates='notes') + +class Tag(Base): + __tablename__ = 'tags' + id = Column(Integer, primary_key=True) + name = Column(String(50), unique=True) + notes = relationship('Note', secondary=note_tag, back_populates='tags') + +class Comment(Base): + __tablename__ = 'comments' + id = Column(Integer, primary_key=True) + content = Column(Text, nullable=False) + post_id = Column(Integer, ForeignKey('notes.id'), nullable=False) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + note = relationship('Note', backref='comments') diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 00000000..8b5d81ed --- /dev/null +++ b/app/routes.py @@ -0,0 +1,111 @@ +from flask import Blueprint, request, jsonify +from .models import Note, Tag, note_tag, Comment +from . import db_session + +notes_bp = Blueprint('notes', __name__, url_prefix='/api/notes') +tags_bp = Blueprint('tags', __name__, url_prefix='/api/tags') +comments_bp = Blueprint('comments', __name__, url_prefix='/api/comments') + +@notes_bp.route('/', methods=['GET']) +def get_notes(): + notes = db_session.query(Note).all() + return jsonify([{'id': n.id, 'title': n.title, 'content': n.content} for n in notes]) + +@notes_bp.route('/', methods=['POST']) +def create_note(): + data = request.json + new_note = Note(title=data['title'], content=data['content']) + db_session.add(new_note) + db_session.commit() + return jsonify({'message': 'Note created'}), 201 + +@notes_bp.route('/', methods=['PUT']) +def update_note(note_id): + data = request.json + note = db_session.query(Note).get(note_id) + if not note: + return jsonify({'error': 'Note not found'}), 404 + note.title = data['title'] + note.content = data['content'] + db_session.commit() + return jsonify({'message': 'Note updated'}) + +@notes_bp.route('/', methods=['DELETE']) +def delete_note(note_id): + note = db_session.query(Note).get(note_id) + if not note: + return jsonify({'error': 'Note not found'}), 404 + db_session.delete(note) + db_session.commit() + return jsonify({'message': 'Note deleted'}) + +@tags_bp.route('/', methods=['POST']) +def create_tag(): + data = request.json + tag = Tag(name=data['name']) + db_session.add(tag) + db_session.commit() + return jsonify({'message': 'Tag created', 'id': tag.id}), 201 + +@tags_bp.route('/', methods=['GET']) +def get_tags(): + tags = db_session.query(Tag).all() + return jsonify([{'id': t.id, 'name': t.name} for t in tags]) + +@tags_bp.route('/', methods=['PUT']) +def update_tag(tag_id): + data = request.json + tag = db_session.query(Tag).get(tag_id) + if not tag: + return jsonify({'error': 'Tag not found'}), 404 + tag.name = data['name'] + db_session.commit() + return jsonify({'message': 'Tag updated'}) + +@tags_bp.route('/', methods=['DELETE']) +def delete_tag(tag_id): + tag = db_session.query(Tag).get(tag_id) + if not tag: + return jsonify({'error': 'Tag not found'}), 404 + db_session.delete(tag) + db_session.commit() + return jsonify({'message': 'Tag deleted'}) + +@comments_bp.route('/', methods=['POST']) +def create_comment(): + data = request.json + comment = Comment(content=data['content'], post_id=data['post_id']) + db_session.add(comment) + db_session.commit() + return jsonify({'message': 'Comment created', 'id': comment.id}), 201 + +@comments_bp.route('/', methods=['GET']) +def get_comments(): + post_id = request.args.get('post_id') + query = db_session.query(Comment) + if post_id: + query = query.filter_by(post_id=post_id) + comments = query.all() + return jsonify([ + {'id': c.id, 'content': c.content, 'post_id': c.post_id, 'created_at': c.created_at.isoformat()} + for c in comments + ]) + +@comments_bp.route('/', methods=['PUT']) +def update_comment(comment_id): + data = request.json + comment = db_session.query(Comment).get(comment_id) + if not comment: + return jsonify({'error': 'Comment not found'}), 404 + comment.content = data['content'] + db_session.commit() + return jsonify({'message': 'Comment updated'}) + +@comments_bp.route('/', methods=['DELETE']) +def delete_comment(comment_id): + comment = db_session.query(Comment).get(comment_id) + if not comment: + return jsonify({'error': 'Comment not found'}), 404 + db_session.delete(comment) + db_session.commit() + return jsonify({'message': 'Comment deleted'}) diff --git a/database.db b/database.db new file mode 100644 index 00000000..7cdd4d4f Binary files /dev/null and b/database.db differ diff --git a/models.py b/duplicate_models.py similarity index 100% rename from models.py rename to duplicate_models.py diff --git a/run.py b/run.py new file mode 100644 index 00000000..a3fdaf3c --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/test_api.py b/test_api.py new file mode 100644 index 00000000..327ae191 --- /dev/null +++ b/test_api.py @@ -0,0 +1,43 @@ +import pytest +import json +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from app import create_app + +@pytest.fixture +def client(): + app = create_app() + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' # Use in-memory DB for tests + with app.test_client() as client: + with app.app_context(): + yield client + +def test_post_note(client): + payload = {"title": "Test Note", "content": "This is a test note."} + response = client.post("/api/notes/", data=json.dumps(payload), content_type="application/json") + assert response.status_code == 201 + assert b'Note created' in response.data + +def test_get_notes(client): + # Ensure at least one note exists + client.post("/api/notes/", data=json.dumps({"title": "Temp", "content": "Temp"}), content_type="application/json") + response = client.get("/api/notes/") + assert response.status_code == 200 + assert isinstance(response.get_json(), list) + +def test_update_note(client): + # Create a note first + client.post("/api/notes/", data=json.dumps({"title": "Temp", "content": "Temp"}), content_type="application/json") + response = client.put("/api/notes/1", data=json.dumps({"title": "Updated", "content": "Updated"}), content_type="application/json") + assert response.status_code == 200 + assert b'Note updated' in response.data + +def test_delete_note(client): + # Create a note first + client.post("/api/notes/", data=json.dumps({"title": "Temp", "content": "Temp"}), content_type="application/json") + response = client.delete("/api/notes/1") + assert response.status_code == 200 + assert b'Note deleted' in response.data diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..3d30eba6 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,40 @@ +import pytest +import json +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from app import create_app() + +@pytest.fixture +def client(): + app = create_app() + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' # Use in-memory DB for tests + with app.test_client() as client: + with app.app_context(): + yield client + +def test_post_note(client): + payload = {"title": "Test Note", "content": "This is a test note."} + response = client.post("/api/notes/", data=json.dumps(payload), content_type="application/json") + assert response.status_code == 201 + assert b'Note created' in response.data + +def test_get_notes(client): + client.post("/api/notes/", data=json.dumps({"title": "Temp", "content": "Temp"}), content_type="application/json") + response = client.get("/api/notes/") + assert response.status_code == 200 + assert isinstance(response.get_json(), list) + +def test_update_note(client): + client.post("/api/notes/", data=json.dumps({"title": "Temp", "content": "Temp"}), content_type="application/json") + response = client.put("/api/notes/1", data=json.dumps({"title": "Updated", "content": "Updated"}), content_type="application/json") + assert response.status_code == 200 + assert b'Note updated' in response.data + +def test_delete_note(client): + client.post("/api/notes/", data=json.dumps({"title": "Temp", "content": "Temp"}), content_type="application/json") + response = client.delete("/api/notes/1") + assert response.status_code == 200 + assert b'Note deleted' in response.data