diff --git a/app/__init__.py b/app/__init__.py index 1f43889..3d85a32 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,6 +10,7 @@ from app.routes_sources import sources_bp from app.routes_musicians import musicians_bp from app.routes_tunings import tunings_bp from app.routes_reference_sites import reference_sites_bp +from app.routes_terms import terms_bp def create_app(): @@ -39,6 +40,7 @@ def create_app(): app.register_blueprint(musicians_bp) app.register_blueprint(tunings_bp) app.register_blueprint(reference_sites_bp) + app.register_blueprint(terms_bp) # --- Error handlers --- from sqlalchemy.exc import IntegrityError diff --git a/app/models.py b/app/models.py index ef6202f..e43bf2d 100644 --- a/app/models.py +++ b/app/models.py @@ -61,6 +61,18 @@ class Tuning(db.Model): return {"id": self.id, "name": self.name} +class Term(db.Model): + __tablename__ = "terms" + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, nullable=False, unique=True) + + entries = db.relationship("TuneByInstrument", back_populates="term") + + def to_dict(self): + return {"id": self.id, "name": self.name} + + class Tune(db.Model): __tablename__ = "tunes" @@ -114,6 +126,7 @@ class TuneByInstrument(db.Model): instrument_id = Column(Integer, ForeignKey("instruments.id", ondelete="CASCADE"), nullable=False) learned_from_id = Column(Integer, ForeignKey("musicians.id", ondelete="SET NULL"), nullable=True) tuning_id = Column(Integer, ForeignKey("tunings.id", ondelete="SET NULL"), nullable=True) + term_id = Column(Integer, ForeignKey("terms.id", ondelete="SET NULL"), nullable=True) date_learned = Column(Date, nullable=True) status = Column(String, nullable=True) difficulty = Column(String, nullable=True) @@ -121,6 +134,7 @@ class TuneByInstrument(db.Model): tune = db.relationship("Tune", back_populates="instruments") instrument = db.relationship("Instrument", back_populates="entries") tuning = db.relationship("Tuning", back_populates="entries") + term = db.relationship("Term", back_populates="entries") learned_from_musician = db.relationship("Musician", back_populates="instrument_entries") notes = db.relationship("TuneByInstrumentNote", back_populates="entry", cascade="all, delete-orphan") @@ -135,6 +149,8 @@ class TuneByInstrument(db.Model): "date_learned": self.date_learned.isoformat() if self.date_learned else None, "tuning_id": self.tuning_id, "tuning": self.tuning.name if self.tuning else None, + "term_id": self.term_id, + "term": self.term.name if self.term else None, "status": self.status, "difficulty": self.difficulty, "notes": [n.to_dict() for n in self.notes], @@ -151,6 +167,8 @@ class TuneByInstrument(db.Model): "date_learned": self.date_learned.isoformat() if self.date_learned else None, "tuning_id": self.tuning_id, "tuning": self.tuning.name if self.tuning else None, + "term_id": self.term_id, + "term": self.term.name if self.term else None, "status": self.status, "difficulty": self.difficulty, } diff --git a/app/routes_instruments.py b/app/routes_instruments.py index 723422e..341bb42 100644 --- a/app/routes_instruments.py +++ b/app/routes_instruments.py @@ -77,6 +77,7 @@ def create_tune_instrument(tune_id): instrument_id=instrument_id, learned_from_id=data.get("learned_from_id"), tuning_id=data.get("tuning_id"), + term_id=data.get("term_id"), date_learned=data.get("date_learned"), status=data.get("status"), difficulty=data.get("difficulty"), @@ -94,7 +95,7 @@ def create_tune_instrument(tune_id): def update_tune_instrument(tune_id, entry_id): entry = db.get_or_404(TuneByInstrument, entry_id) data = request.get_json(force=True) - fields = ("instrument_id", "learned_from_id", "tuning_id", "date_learned", + fields = ("instrument_id", "learned_from_id", "tuning_id", "term_id", "date_learned", "status", "difficulty") for field in fields: if field in data: diff --git a/app/routes_terms.py b/app/routes_terms.py new file mode 100644 index 0000000..20fff5c --- /dev/null +++ b/app/routes_terms.py @@ -0,0 +1,43 @@ +"""Routes for terms lookup table.""" +from flask import Blueprint, jsonify, request +from app.extensions import db +from app.models import Term +from app.auth import require_api_key + +terms_bp = Blueprint("terms", __name__, url_prefix="/terms") + + +@terms_bp.get("/") +def list_terms(): + terms = db.session.query(Term).order_by(Term.name).all() + return jsonify([t.to_dict() for t in terms]) + + +@terms_bp.post("/") +@require_api_key +def create_term(): + data = request.get_json(force=True) + term = Term(name=data["name"]) + db.session.add(term) + db.session.commit() + return jsonify(term.to_dict()), 201 + + +@terms_bp.patch("/") +@require_api_key +def update_term(term_id): + term = db.get_or_404(Term, term_id) + data = request.get_json(force=True) + if "name" in data: + term.name = data["name"] + db.session.commit() + return jsonify(term.to_dict()) + + +@terms_bp.delete("/") +@require_api_key +def delete_term(term_id): + term = db.get_or_404(Term, term_id) + db.session.delete(term) + db.session.commit() + return jsonify({"deleted": term_id}) diff --git a/migrations/versions/b1c2d3e4f5a6_add_terms.py b/migrations/versions/b1c2d3e4f5a6_add_terms.py new file mode 100644 index 0000000..e4dc25c --- /dev/null +++ b/migrations/versions/b1c2d3e4f5a6_add_terms.py @@ -0,0 +1,35 @@ +"""add terms table and term_id to tune_by_instrument + +Revision ID: b1c2d3e4f5a6 +Revises: f1a2b3c4d5e6 +Create Date: 2026-06-09 +""" +from alembic import op +import sqlalchemy as sa + +revision = "b1c2d3e4f5a6" +down_revision = "f1a2b3c4d5e6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "terms", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column("name", sa.String(), nullable=False, unique=True), + ) + op.add_column( + "tune_by_instrument", + sa.Column( + "term_id", + sa.Integer(), + sa.ForeignKey("terms.id", ondelete="SET NULL"), + nullable=True, + ), + ) + + +def downgrade() -> None: + op.drop_column("tune_by_instrument", "term_id") + op.drop_table("terms")