Skip to content

Tutorial Quick Notes

level09 edited this page Dec 30, 2025 · 2 revisions

Add Quick Notes to Your Dashboard

Add a personal notes feature to your dashboard. Users can create, view, and delete notes.

Time: ~10 minutes


What You'll Build

  • Note model linked to users
  • API endpoints for CRUD operations
  • Notes displayed as Vuetify cards on the dashboard

Step 1: Create the Note Model

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 self

Pattern notes:

  • Inherits from db.Model
  • to_dict() for JSON serialization
  • from_dict() for updating from request data
  • Foreign key links to the User model

Step 2: Add API Endpoints

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 "", 204

Pattern notes:

  • Portal blueprint already has @auth_required("session") applied via before_request
  • SQLAlchemy 2.x uses db.select() and db.session.execute()
  • Uses db.session.add() and db.session.delete() directly (matching existing patterns)
  • Always filter by user_id so users only see their own notes

Step 3: Update the Dashboard Template

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 in config.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

Step 4: Reset Database and Test

rm instance/enferno.db
uv run flask create-db
uv run flask install
uv run flask run

Visit http://localhost:5000/dashboard/:

  1. Add a note with title and content
  2. See it appear as a card
  3. Delete it with the trash icon

Extending

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).


Troubleshooting

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)

← Back to Tutorials