-
-
Notifications
You must be signed in to change notification settings - Fork 79
Tutorial Quick Notes
Add a personal notes feature to your dashboard. Users can create, view, and delete notes.
Time: ~10 minutes
-
Notemodel linked to users - API endpoints for CRUD operations
- Notes displayed as Vuetify cards on the dashboard
Prompt for AI assistant:
Create a Note model in enferno/portal/models.py with fields: id, title, content, created_at, user_id (foreign key to User). Follow the existing model patterns with to_dict and from_dict methods.
Create file enferno/portal/models.py:
from datetime import datetime
from enferno.extensions import db
class Note(db.Model):
__tablename__ = "notes"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"content": self.content,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
def from_dict(self, data):
self.title = data.get("title", self.title)
self.content = data.get("content", self.content)
return selfPattern notes:
- Inherits from
db.Model -
to_dict()for JSON serialization -
from_dict()for updating from request data - Foreign key links to the User model
Prompt for AI assistant:
Add API endpoints to enferno/portal/views.py for notes: GET /api/notes (list current user's notes), POST /api/notes (create note), DELETE /api/notes/ (delete note). Use SQLAlchemy 2.x patterns matching the existing user views.
Update enferno/portal/views.py:
from flask import Blueprint, jsonify, request
from flask.templating import render_template
from flask_security import auth_required, current_user
from enferno.extensions import db
from enferno.portal.models import Note
from enferno.user.models import Activity, Role, User
portal = Blueprint("portal", __name__, static_folder="../static")
@portal.before_request
@auth_required("session")
def before_request():
pass
@portal.after_request
def add_header(response):
response.headers["Cache-Control"] = "public, max-age=10800"
return response
@portal.route("/dashboard/")
def dashboard():
stats = {
"users": User.query.count(),
"roles": Role.query.count(),
"activities": Activity.query.count(),
}
return render_template("dashboard.html", stats=stats)
@portal.route("/api/notes")
def list_notes():
query = db.select(Note).where(
Note.user_id == current_user.id
).order_by(Note.created_at.desc())
notes = db.session.execute(query).scalars().all()
return jsonify([n.to_dict() for n in notes])
@portal.route("/api/notes", methods=["POST"])
def create_note():
data = request.json
note = Note(
title=data.get("title", ""),
content=data.get("content", ""),
user_id=current_user.id
)
db.session.add(note)
db.session.commit()
return jsonify(note.to_dict()), 201
@portal.route("/api/notes/<int:note_id>", methods=["DELETE"])
def delete_note(note_id):
note = db.session.execute(
db.select(Note).where(Note.id == note_id, Note.user_id == current_user.id)
).scalar_one_or_none()
if not note:
return jsonify({"error": "Not found"}), 404
db.session.delete(note)
db.session.commit()
return "", 204Pattern notes:
- Portal blueprint already has
@auth_required("session")applied viabefore_request - SQLAlchemy 2.x uses
db.select()anddb.session.execute() - Uses
db.session.add()anddb.session.delete()directly (matching existing patterns) - Always filter by
user_idso users only see their own notes
Prompt for AI assistant:
Update enferno/templates/dashboard.html to show notes. Add a form with title and content fields, a button to add notes, and display existing notes as Vuetify cards in a grid. Use Vue 3 with the existing config.delimiters pattern.
Add to enferno/templates/dashboard.html (inside v-container, after the Quick Links card):
<!-- Notes Section -->
<v-row class="mt-6">
<v-col cols="12">
<v-card>
<v-card-title>My Notes</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<v-text-field
v-model="noteTitle"
label="Title"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="noteContent"
label="Content"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" md="2">
<v-btn color="primary" @click="addNote" block>
<i class="ti ti-plus mr-1"></i> Add
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col v-for="note in notes" :key="note.id" cols="12" sm="6" md="4">
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
${ note.title }
<v-btn icon size="small" variant="text" @click="deleteNote(note.id)">
<i class="ti ti-trash"></i>
</v-btn>
</v-card-title>
<v-card-text>${ note.content }</v-card-text>
</v-card>
</v-col>
</v-row>Update the Vue app in the {% block js %} section:
const app = createApp({
mixins: [layoutMixin],
data() {
return {
notes: [],
noteTitle: '',
noteContent: ''
};
},
methods: {
async loadNotes() {
const res = await axios.get('/dashboard/api/notes');
this.notes = res.data;
},
async addNote() {
if (!this.noteTitle) return;
await axios.post('/dashboard/api/notes', {
title: this.noteTitle,
content: this.noteContent
});
this.noteTitle = '';
this.noteContent = '';
this.loadNotes();
},
async deleteNote(id) {
await axios.delete(`/dashboard/api/notes/${id}`);
this.loadNotes();
}
},
mounted() {
this.loadNotes();
},
delimiters: config.delimiters
});Pattern notes:
- Uses
${ }delimiters (configured inconfig.js) to avoid Jinja conflicts - Vuetify 3 components (VTextField uses
variant="outlined"by default via config) - Tabler icons (
ti ti-*) - Axios is loaded globally in
layout.html
rm instance/enferno.db
uv run flask create-db
uv run flask install
uv run flask runVisit http://localhost:5000/dashboard/:
- Add a note with title and content
- See it appear as a card
- Delete it with the trash icon
Add note colors:
Add a color field to Note with choices like 'blue', 'green', 'yellow'. Add a color picker in the form and apply the color as card background.
Add search:
Add a search input that filters displayed notes by title as the user types (client-side filter).
| Issue | Solution |
|---|---|
| Import errors | Check imports at top of views.py and create models.py
|
| Database errors | Delete instance/enferno.db and run flask create-db
|
| Notes not loading | Check browser console for API errors, verify route is /dashboard/api/notes
|
| Vue not updating | Hard refresh (Cmd+Shift+R) |