diff --git a/README.md b/README.md index a30c837e..1eddc65c 100644 --- a/README.md +++ b/README.md @@ -1,283 +1,187 @@ -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) - -## Welcome - -Hello. Want to get started with Flask quickly? Good. You came to the right place. This Flask application framework is pre-configured with **Flask-SQLAlchemy**, **Flask-WTF**, **Fabric**, **Coverage**, and the **Bootstrap** frontend (among others). This will get your Flask app up and running on Heroku or PythonAnywhere quickly. Use this starter, boilerplate for all you new Flask projects. Cheers! - -
- -![real-python-logo](https://raw.githubusercontent.com/realpython/about/master/rp_small.png) - -**Designed for the [Real Python](http://www.realpython.com) course.** - -
- -Preview the skeleton app here - [http://www.flaskboilerplate.com/](http://www.flaskboilerplate.com/) - -**EXAMPLE APP: [http://flasktaskr.herokuapp.com/](http://flasktaskr.herokuapp.com/)** - -**What is Flask?** Flask is a microframework for Python based on Werkzeug and Jinja2. - -Project Structure --------- - - ```sh - ├── Procfile - ├── Procfile.dev - ├── README.md - ├── app.py - ├── config.py - ├── error.log - ├── forms.py - ├── models.py - ├── requirements.txt - ├── static - │   ├── css - │   │   ├── bootstrap-3.0.0.min.css - │   │   ├── bootstrap-theme-3.0.0.css - │   │   ├── bootstrap-theme-3.0.0.min.css - │   │   ├── font-awesome-3.2.1.min.css - │   │   ├── layout.forms.css - │   │   ├── layout.main.css - │   │   ├── main.css - │   │   ├── main.quickfix.css - │   │   └── main.responsive.css - │   ├── font - │   │   ├── FontAwesome.otf - │   │   ├── fontawesome-webfont.eot - │   │   ├── fontawesome-webfont.svg - │   │   ├── fontawesome-webfont.ttf - │   │   └── fontawesome-webfont.woff - │   ├── ico - │   │   ├── apple-touch-icon-114-precomposed.png - │   │   ├── apple-touch-icon-144-precomposed.png - │   │   ├── apple-touch-icon-57-precomposed.png - │   │   ├── apple-touch-icon-72-precomposed.png - │   │   └── favicon.png - │   ├── img - │   └── js - │   ├── libs - │   │   ├── bootstrap-3.0.0.min.js - │   │   ├── jquery-1.10.2.min.js - │   │   ├── modernizr-2.6.2.min.js - │   │   └── respond-1.3.0.min.js - │   ├── plugins.js - │   └── script.js - └── templates - ├── errors - │   ├── 404.html - │   └── 500.html - ├── forms - │   ├── forgot.html - │   ├── login.html - │   └── register.html - ├── layouts - │   ├── form.html - │   └── main.html - └── pages - ├── placeholder.about.html - └── placeholder.home.html - ``` - -### Screenshots - -![Pages](https://github.com/realpython/flask-boilerplate/blob/master/screenshots/pages.png) - -![Forms](https://github.com/realpython/flask-boilerplate/blob/master/screenshots/forms.png) - - -### Quick Start - -1. Clone the repo - ``` - $ git clone https://github.com/realpython/flask-boilerplate.git - $ cd flask-boilerplate - ``` - -2. Initialize and activate a virtualenv: - ``` - $ virtualenv --no-site-packages env - $ source env/bin/activate - ``` - -3. Install the dependencies: - ``` - $ pip install -r requirements.txt - ``` - -5. Run the development server: - ``` - $ python app.py - ``` - -6. Navigate to [http://localhost:5000](http://localhost:5000) - - -Deploying to Heroku ------- - -1. Signup for [Heroku](https://api.heroku.com/signup) -2. Login to Heroku and download the [Heroku Toolbelt](https://toolbelt.heroku.com/) -3. Once installed, open your command-line and run the following command - `heroku login`. Then follow the prompts: - - ``` - Enter your Heroku credentials. - Email: michael@mherman.org - Password (typing will be hidden): - Could not find an existing public key. - Would you like to generate one? [Yn] - Generating new SSH public key. - Uploading ssh public key /Users/michaelherman/.ssh/id_rsa.pub - ``` - -4. Activate your virtualenv -5. Heroku recognizes the dependencies needed through a *requirements.txt* file. Create one using the following command: `pip freeze > requirements.txt`. Now, this will only create the dependencies from the libraries you installed using pip. If you used easy_install, you will need to add them directly to the file. -6. Create a Procfile. Open up a text editor and save the following text in it: - - ``` - web: gunicorn app:app --log-file=- - ``` - - Then save the file in your applications root or main directory as *Procfile* (no extension). The word "web" indicates to Heroku that the application will be attached to the HTTP routing stack once deployed. - -7. Create a local Git repository (if necessary): - - ``` - $ git init - $ git add . - $ git commit -m "initial files" - ``` - -8. Create your app on Heroku: - - ``` - $ heroku create - ``` - -9. Deploy your code to Heroku: - - ``` - $ git push heroku master - ``` - -10. View the app in your browser: - - ``` - $ heroku open - ``` - -11. You app should look similar to this - [http://www.flaskboilerplate.com/](http://www.flaskboilerplate.com/) - -12. Having problems? Look at the Heroku error log: - - ``` - $ heroku logs - ``` - -### Deploying to PythonAnywhere - -1. Install [Git](http://git-scm.com/downloads) and [Python](http://install.python-guide.org/) - if you don't already have them, of course. - - > If you plan on working exclusively within PythonAnywhere, which you can, because it provides a cloud solution for hosting and developing your application, you can skip step one entirely. :) - -2. Sign up for [PythonAnywhere](https://www.pythonanywhere.com/pricing/), if you haven't already -3. Once logged in, you should be on the Consoles tab. -4. Clone this repo: - ``` - $ git clone git://github.com/realpython/flask-boilerplate.git - $ cd flask-boilerplate - ``` - -5. Create and activate a virtualenv: - ``` - $ virtualenv venv --no-site-packages - $ source venv/bin/activate - ``` - -6. Install requirements: - ``` - $ pip install -r requirements.txt - ``` - -7. Next, back on PythonAnywhere, click Web tab. -8. Click the "Add a new web app" link on the left; by default this will create an app at your-username.pythonanywhere.com, though if you've signed up for a paid "Web Developer" account you can also specify your own domain name here. Once you've decided on the location of the app, click the "Next" button. -9. On the next page, click the "Flask" option, and on the next page just keep the default settings and click "Next" again. -Once the web app has been created (it'll take 20 seconds or so), you'll see a link near the top of the page, under the "Reload web app" button, saying "It is configured via a WSGI file stored at..." and a filename. Click this, and you get to a page with a text editor. -10. Put the following lines of code at the start of the WSGI file (changing "your-username" appropriately) - - ``` - activate_this = '/home/your-username/flask-boilerplate/venv/bin/activate_this.py' - execfile(activate_this, dict(__file__=activate_this)) - ``` - -11. Then update the following lines of code: - - from - - ``` - project_home = u'/home/your-username/mysite' - ``` - - to - - ``` - project_home = u'/home/your-username/flask-boilerplate' - ``` - - from - - ``` - from flask_app import app as application - ``` - - to - - ``` - from app import app as application - ``` - -12. Save the file. -13. Go to the website http://your-username.pythonanywhere.com/ (or your own domain if you specified a different one earlier), and you should see something like this - [http://www.flaskboilerplate.com/](http://www.flaskboilerplate.com/). - -*Now you're ready to start developing!* - -***Need to PUSH your PythonAnywhere repo to Github?*** - -1. Start a bash console -2. Run: - - ``` - $ ssh-keygen -t rsa - ``` - -3. Just accept the defaults, then show the public key: - - ``` - $ cat ~/.ssh/id_rsa.pub - ``` - -4. Log in to GitHub. -5. Go to the "Account settings" option at the top right (currently a wrench and a screwdriver crossed) -6. Select "SSH Keys" from the list at the left. -7. Click the "Add SSH key" button at top right. -8. Enter a title (I suggest something like "From PythonAnywhere" and then paste the output of the previous "cat" command into the Key box. -9. Click the green "Add key" button. You'll be prompted to enter your password. - -PUSH and PULL away! - -### What's next? - -1. Using Heroku? Make sure you deactivate your virtualenv once you're done deploying: `deactivate` -2. Need to reactivate? (1) Unix - `source venv/bin/activate` (2) Windows - `venv\scripts\activate` -4. Add your Google Analytics ID to the *main.html* file -5. Add a domain name to [Heroku](https://devcenter.heroku.com/articles/custom-domains) or PythonAnywhere via a [CNAME](http://en.wikipedia.org/wiki/CNAME_record) record -5. DEVELOP YOUR APP - need [help](http://realpython.com)? - -### Learn More - -1. [Getting Started with Python on Heroku](https://devcenter.heroku.com/articles/python) -2. [PythonAnywhere - Help](https://www.pythonanywhere.com/help/) -1. [Flask Documentation](http://flask.pocoo.org/docs/) -2. [Flask Extensions](http://flask.pocoo.org/extensions/) -1. [Real Python](http://www.realpythonfortheweb.com) :) - +# Note-Taking Flask Application + +A comprehensive note-taking web application built with Flask, featuring full CRUD operations and REST API endpoints. + +## Features + +### Core Features +- **Create, Read, Update, Delete (CRUD)** operations for notes +- **Tagging system** for organizing notes +- **Search functionality** across titles and content +- **RESTful API** with full CRUD endpoints +- **Responsive web interface** +- **SQLite database** for data persistence + +### API Endpoints + +#### Notes API +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/notes` | Get all notes (supports search and tag filtering) | +| GET | `/api/notes/` | Get a specific note by ID | +| POST | `/api/notes` | Create a new note | +| PUT | `/api/notes/` | Update an existing note | +| DELETE | `/api/notes/` | Delete a note | + +#### Tags API +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/tags` | Get all unique tags | + +### Note Structure +```json +{ + "id": 1, + "title": "My Note Title", + "content": "Note content goes here...", + "tags": "work,important,flask", + "created_at": "2024-01-15T10:30:00", + "updated_at": "2024-01-15T14:45:00" +} +``` + +## Installation + +1. **Clone the repository** + ```bash + git clone + cd note-taking_flask_app + ``` + +2. **Create virtual environment** + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. **Install dependencies** + ```bash + pip install -r requirements.txt + ``` + +4. **Initialize database** + ```bash + python -c "from models import Base, engine; Base.metadata.create_all(bind=engine)" + ``` + +5. **Run the application** + ```bash + python app.py + ``` + +## Usage + +### Web Interface +- Visit `http://localhost:5000` to access the web interface +- Use the navigation to access different pages + +### API Usage Examples + +#### Create a new note +```bash +curl -X POST http://localhost:5000/api/notes \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My First Note", + "content": "This is the content of my first note", + "tags": "personal,important" + }' +``` + +#### Get all notes +```bash +curl http://localhost:5000/api/notes +``` + +#### Search notes +```bash +curl "http://localhost:5000/api/notes?search=flask" +``` + +#### Get notes by tag +```bash +curl "http://localhost:5000/api/notes?tag=work" +``` + +#### Update a note +```bash +curl -X PUT http://localhost:5000/api/notes/1 \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Updated Note Title", + "content": "Updated content" + }' +``` + +#### Delete a note +```bash +curl -X DELETE http://localhost:5000/api/notes/1 +``` + +## Database Schema + +### Notes Table +- `id` (INTEGER, PRIMARY KEY) +- `title` (VARCHAR(200), NOT NULL) +- `content` (TEXT, NOT NULL) +- `tags` (VARCHAR(500), NULLABLE) +- `created_at` (DATETIME, DEFAULT: current timestamp) +- `updated_at` (DATETIME, DEFAULT: current timestamp, ON UPDATE: current timestamp) + +## Development + +### Project Structure +``` +note-taking_flask_app/ +├── app.py # Main application file +├── api.py # REST API endpoints +├── models.py # Database models +├── forms.py # WTForms definitions +├── config.py # Configuration settings +├── requirements.txt # Python dependencies +├── database.db # SQLite database (created on first run) +├── README.md # This file +├── templates/ # HTML templates +├── static/ # CSS, JS, and image files +└── error.log # Application logs +``` + +### Adding New Features +1. Update the database model in `models.py` +2. Add corresponding API endpoints in `api.py` +3. Update the web interface in `app.py` +4. Add appropriate tests + +### Testing +The application includes basic error handling and validation. For comprehensive testing: +- Test all CRUD operations via API +- Verify tag filtering and search functionality +- Check error handling for invalid inputs +- Test database transactions + +## Deployment + +### Using Heroku +1. Create a Heroku app +2. Set environment variables +3. Deploy using Git + +### Using Docker +```dockerfile +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY . . +CMD ["python", "app.py"] +``` + +## Contributing +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Submit a pull request + +## License +This project is open source and available under the [MIT License](LICENSE). diff --git a/api.py b/api.py new file mode 100644 index 00000000..5784cbd6 --- /dev/null +++ b/api.py @@ -0,0 +1,310 @@ +#----------------------------------------------------------------------------# +# Imports +#----------------------------------------------------------------------------# +from flask import Blueprint, jsonify, request +from models import Note, db_session +from datetime import datetime + +#----------------------------------------------------------------------------# +# API Blueprint +#----------------------------------------------------------------------------# +api_bp = Blueprint('api', __name__, url_prefix='/api') + +#----------------------------------------------------------------------------# +# Helper Functions +#----------------------------------------------------------------------------# + +def validate_note_data(data): + """Validate note data from request""" + errors = [] + + if not data.get('title') or not data.get('title').strip(): + errors.append('Title is required') + + if not data.get('content') or not data.get('content').strip(): + errors.append('Content is required') + + return errors + +#----------------------------------------------------------------------------# +# REST API Endpoints +#----------------------------------------------------------------------------# + +# GET /api/notes - Get all notes +@api_bp.route('/notes', methods=['GET']) +def get_notes(): + """ + Get all notes + + Query Parameters: + search: Search term to filter notes + tag: Filter notes by tag + + Returns: + JSON array of all notes + """ + try: + search_term = request.args.get('search') + tag_filter = request.args.get('tag') + + if search_term: + notes = Note.search_notes(search_term) + elif tag_filter: + notes = Note.get_notes_by_tag(tag_filter) + else: + notes = Note.get_all_notes() + + return jsonify({ + 'success': True, + 'data': [note.to_dict() for note in notes], + 'count': len(notes) + }), 200 + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +# GET /api/notes/ - Get a single note +@api_bp.route('/notes/', methods=['GET']) +def get_note(note_id): + """ + Get a single note by ID + + Args: + note_id: The ID of the note to retrieve + + Returns: + JSON object of the note + """ + try: + note = Note.get_note_by_id(note_id) + + if not note: + return jsonify({ + 'success': False, + 'error': 'Note not found' + }), 404 + + return jsonify({ + 'success': True, + 'data': note.to_dict() + }), 200 + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +# POST /api/notes - Create a new note +@api_bp.route('/notes', methods=['POST']) +def create_note(): + """ + Create a new note + + Request Body: + title: Note title (required) + content: Note content (required) + tags: Comma-separated tags (optional) + + Returns: + JSON object of the created note + """ + try: + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': 'No data provided' + }), 400 + + # Validate data + errors = validate_note_data(data) + if errors: + return jsonify({ + 'success': False, + 'errors': errors + }), 400 + + # Create note + note = Note.create_note( + title=data['title'].strip(), + content=data['content'].strip(), + tags=data.get('tags', '').strip() if data.get('tags') else None + ) + + return jsonify({ + 'success': True, + 'data': note.to_dict(), + 'message': 'Note created successfully' + }), 201 + + except Exception as e: + db_session.rollback() + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +# PUT /api/notes/ - Update a note +@api_bp.route('/notes/', methods=['PUT']) +def update_note(note_id): + """ + Update an existing note + + Args: + note_id: The ID of the note to update + + Request Body: + title: Note title (optional) + content: Note content (optional) + tags: Comma-separated tags (optional) + + Returns: + JSON object of the updated note + """ + try: + note = Note.get_note_by_id(note_id) + + if not note: + return jsonify({ + 'success': False, + 'error': 'Note not found' + }), 404 + + data = request.get_json() + + if not data: + return jsonify({ + 'success': False, + 'error': 'No data provided' + }), 400 + + # Update fields if provided + title = data.get('title') + content = data.get('content') + tags = data.get('tags') + + # Validate if title or content are provided + if title is not None and not title.strip(): + return jsonify({ + 'success': False, + 'error': 'Title cannot be empty' + }), 400 + + if content is not None and not content.strip(): + return jsonify({ + 'success': False, + 'error': 'Content cannot be empty' + }), 400 + + updated_note = Note.update_note( + note_id, + title=title.strip() if title else None, + content=content.strip() if content else None, + tags=tags.strip() if tags else None + ) + + return jsonify({ + 'success': True, + 'data': updated_note.to_dict(), + 'message': 'Note updated successfully' + }), 200 + + except Exception as e: + db_session.rollback() + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +# DELETE /api/notes/ - Delete a note +@api_bp.route('/notes/', methods=['DELETE']) +def delete_note(note_id): + """ + Delete a note + + Args: + note_id: The ID of the note to delete + + Returns: + Success message + """ + try: + note = Note.get_note_by_id(note_id) + + if not note: + return jsonify({ + 'success': False, + 'error': 'Note not found' + }), 404 + + success = Note.delete_note(note_id) + + if success: + return jsonify({ + 'success': True, + 'message': 'Note deleted successfully' + }), 200 + else: + return jsonify({ + 'success': False, + 'error': 'Failed to delete note' + }), 500 + + except Exception as e: + db_session.rollback() + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +# GET /api/tags - Get all unique tags +@api_bp.route('/tags', methods=['GET']) +def get_tags(): + """ + Get all unique tags from all notes + + Returns: + JSON array of unique tags + """ + try: + notes = Note.get_all_notes() + all_tags = set() + + for note in notes: + if note.tags: + tags = [tag.strip() for tag in note.tags.split(',')] + all_tags.update(tags) + + return jsonify({ + 'success': True, + 'data': sorted(list(all_tags)) + }), 200 + + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +#----------------------------------------------------------------------------# +# Error Handlers +#----------------------------------------------------------------------------# + +@api_bp.errorhandler(404) +def not_found(error): + return jsonify({ + 'success': False, + 'error': 'Endpoint not found' + }), 404 + +@api_bp.errorhandler(500) +def internal_error(error): + return jsonify({ + 'success': False, + 'error': 'Internal server error' + }), 500 diff --git a/app.py b/app.py index 64d2796c..1cd78077 100644 --- a/app.py +++ b/app.py @@ -1,41 +1,35 @@ #----------------------------------------------------------------------------# # Imports #----------------------------------------------------------------------------# - -from flask import Flask, render_template, request -# from flask.ext.sqlalchemy import SQLAlchemy +from flask import Flask, render_template, request, jsonify +from flask_cors import CORS # Add CORS support for API import logging from logging import Formatter, FileHandler from forms import * import os +# Import API and models +from api import api_bp +from models import Note, db_session + #----------------------------------------------------------------------------# # App Config. #----------------------------------------------------------------------------# - app = Flask(__name__) app.config.from_object('config') -#db = SQLAlchemy(app) +CORS(app) # Enable CORS for all routes -# Automatically tear down SQLAlchemy. -''' -@app.teardown_request +# Register API blueprint +app.register_blueprint(api_bp) + +#----------------------------------------------------------------------------# +# Database Teardown +#----------------------------------------------------------------------------# +@app.teardown_appcontext def shutdown_session(exception=None): + """Remove database session after each request""" db_session.remove() -''' -# Login required decorator. -''' -def login_required(test): - @wraps(test) - def wrap(*args, **kwargs): - if 'logged_in' in session: - return test(*args, **kwargs) - else: - flash('You need to login first.') - return redirect(url_for('login')) - return wrap -''' #----------------------------------------------------------------------------# # Controllers. #----------------------------------------------------------------------------# diff --git a/database.db b/database.db new file mode 100644 index 00000000..8bf5bd15 Binary files /dev/null and b/database.db differ diff --git a/forms.py b/forms.py index 3be12d82..27a3ce7b 100644 --- a/forms.py +++ b/forms.py @@ -1,15 +1,15 @@ from flask_wtf import Form -from wtforms import TextField, PasswordField +from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, EqualTo, Length # Set your classes here. class RegisterForm(Form): - name = TextField( + name = StringField( 'Username', validators=[DataRequired(), Length(min=6, max=25)] ) - email = TextField( + email = StringField( 'Email', validators=[DataRequired(), Length(min=6, max=40)] ) password = PasswordField( @@ -23,11 +23,11 @@ class RegisterForm(Form): class LoginForm(Form): - name = TextField('Username', [DataRequired()]) + name = StringField('Username', [DataRequired()]) password = PasswordField('Password', [DataRequired()]) class ForgotForm(Form): - email = TextField( + email = StringField( 'Email', validators=[DataRequired(), Length(min=6, max=40)] ) diff --git a/models.py b/models.py index e438d665..b250d284 100644 --- a/models.py +++ b/models.py @@ -1,9 +1,14 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker +#----------------------------------------------------------------------------# +# Imports +#----------------------------------------------------------------------------# +from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey +from sqlalchemy.orm import scoped_session, sessionmaker, relationship from sqlalchemy.ext.declarative import declarative_base -# from sqlalchemy import Column, Integer, String -# from app import db +from datetime import datetime +#----------------------------------------------------------------------------# +# Database Setup +#----------------------------------------------------------------------------# engine = create_engine('sqlite:///database.db', echo=True) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, @@ -11,21 +16,103 @@ Base = declarative_base() Base.query = db_session.query_property() -# Set your classes here. +#----------------------------------------------------------------------------# +# Models +#----------------------------------------------------------------------------# -''' -class User(Base): - __tablename__ = 'Users' +class Note(Base): + """ + Note Model + Represents a note in the note-taking application + + Attributes: + id (int): Primary key + title (str): Note title + content (str): Note content/body + tags (str): Comma-separated tags for categorization + created_at (datetime): Creation timestamp + updated_at (datetime): Last update timestamp + """ + __tablename__ = 'notes' + + id = Column(Integer, primary_key=True) + title = Column(String(200), nullable=False) + content = Column(Text, nullable=False) + tags = Column(String(500), nullable=True) # Comma-separated tags + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' + + def to_dict(self): + """Convert note object to dictionary for JSON serialization""" + return { + 'id': self.id, + 'title': self.title, + 'content': self.content, + 'tags': self.tags, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } + + @classmethod + def create_note(cls, title, content, tags=None): + """Create a new note""" + note = cls(title=title, content=content, tags=tags) + db_session.add(note) + db_session.commit() + return note + + @classmethod + def get_all_notes(cls): + """Get all notes ordered by creation date (newest first)""" + return cls.query.order_by(cls.created_at.desc()).all() + + @classmethod + def get_note_by_id(cls, note_id): + """Get a single note by ID""" + return cls.query.get(note_id) + + @classmethod + def update_note(cls, note_id, title=None, content=None, tags=None): + """Update an existing note""" + note = cls.get_note_by_id(note_id) + if note: + if title is not None: + note.title = title + if content is not None: + note.content = content + if tags is not None: + note.tags = tags + note.updated_at = datetime.utcnow() + db_session.commit() + return note + return None + + @classmethod + def delete_note(cls, note_id): + """Delete a note by ID""" + note = cls.get_note_by_id(note_id) + if note: + db_session.delete(note) + db_session.commit() + return True + return False + + @classmethod + def search_notes(cls, search_term): + """Search notes by title or content""" + search_pattern = f"%{search_term}%" + return cls.query.filter( + cls.title.ilike(search_pattern) | cls.content.ilike(search_pattern) + ).order_by(cls.created_at.desc()).all() + + @classmethod + def get_notes_by_tag(cls, tag): + """Get all notes with a specific tag""" + tag_pattern = f"%{tag}%" + return cls.query.filter(cls.tags.ilike(tag_pattern)).order_by(cls.created_at.desc()).all() - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(120), unique=True) - email = db.Column(db.String(120), unique=True) - password = db.Column(db.String(30)) - - def __init__(self, name=None, password=None): - self.name = name - self.password = password -''' - -# Create tables. +# Create tables Base.metadata.create_all(bind=engine) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..c34cae2f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[tool:pytest] +testpaths = . +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short --strict-markers +markers = + unit: Unit tests + integration: Integration tests + api: API tests + model: Model tests diff --git a/requirements.txt b/requirements.txt index 597021e6..d90ef4de 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,30 @@ -Fabric -Flask-SQLAlchemy -Flask-WTF -WTForms -coverage -# ecdsa -# paramiko -# pycrypto +# Core Flask dependencies +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +Flask-WTF==1.1.1 +Flask-CORS==4.0.0 + +# Database +SQLAlchemy==2.0.21 + +# Forms and validation +WTForms==3.0.1 + +# Template engine +Jinja2==3.1.2 +MarkupSafe==2.1.3 + +# Security +Werkzeug==2.3.7 +itsdangerous==2.1.2 + +# Development utilities +python-dotenv==1.0.0 +coverage==7.3.2 + +# Optional: For enhanced JSON handling +flask-marshmallow==0.15.0 +marshmallow-sqlalchemy==0.29.0 + +# Deployment +Fabric==3.2.2 diff --git a/templates/notes.html b/templates/notes.html new file mode 100644 index 00000000..1587835e --- /dev/null +++ b/templates/notes.html @@ -0,0 +1,267 @@ +{% extends "layouts/main.html" %} + +{% block content %} +
+
+
+

Notes Dashboard

+

Create, manage, and organize your notes with tags

+
+
+ + +
+
+
+
+

Create New Note

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+ + +
+
+

Your Notes

+
+ +
+
+ +
+
+
+
+ + + + +