Skip to content

Commit

Permalink
Merge pull request #274 from jwjacobson/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
jwjacobson authored Jun 16, 2024
2 parents fcadc77 + b18f2d1 commit 5378fc0
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 27 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@ This page will help you if you're not sure what tune you want to play. It select
This page consists of precreated "public" tunes that you can copy into your repertoire by clicking the Take button. Right now it has 200 tunes but I hope to make it more comprehensive eventually. Let me know if you find any mistakes!

#### Searching tunes
The Home, Play, and Public pages all have a search box. Terms are searched across all fields. It uses AND logic if you put in more than one term. For example, if you search "monk" you'll get all your Monk tunes, but if you search "monk bud" you'll only get "In Walked Bud" (if it's in your repertoire). Searches are not case sensitive.
The Home, Play, and Public pages all have a search box. Terms are searched across all fields. It uses AND logic if you put in more than one term. For example, if you search ```monk``` you'll get all your Monk tunes, but if you search ```monk bud``` you'll only get "In Walked Bud" (if it's in your repertoire). Searches are not case sensitive.

Out of respect, all the jazz composers are indexed by last name, but there's also nickname substitution working behind the scenes. This means, for example, that Miles Davis is listed as "Davis" in the Composer column, but if you search "miles" you will still get all his tunes. Other "nicknamed" jazz composers include Duke Ellington ("duke"), Bud Powell ("bud"), Charlie Parker ("bird"), etc., covering at least everyone in the Public database with a standard nickname. Let me know if I've missed any.
Out of respect, all the jazz composers are indexed by last name, but there's also nickname substitution working behind the scenes. This means, for example, that Miles Davis is listed as "Davis" in the Composer column, but if you search ```miles``` you will still get all his tunes. Other "nicknamed" jazz composers include Duke Ellington ("duke"), Bud Powell ("bud"), Charlie Parker ("bird"), etc., covering at least everyone in the Public database with a standard nickname. Let me know if I've missed any.

The "Haven't played in" dropdown on the Home and Play pages lets you filter by how recently you've played tunes. So if you select "a day," you'll get back all the tunes you haven't played in the last day (which should be most of your repertoire). The more you keep your plays updated, the more useful this feature is.

You can exclude a term from your search by using a minus sign just before the term. So the search ```-blues``` will exclude tunes containing "blues" in any field from your search.

You can now search specific fields using the format ```field:term```. This is especially useful when searching for keys and forms. Currently supported fields are title, composer, key (just the "Key" column, the tune's main key), *keys* (both "Key" and "Other Keys"), form, style, meter, and year. Most fields have relatively exclusive content types, which means you can do a lot with just basic search (e.g., if you search "love," that term will only be relevant to the Title field.) Keys are more difficult, since they have a lot of overlap with other fields. Before, if you searched "Ab" for the key Ab, you would also get any titles or composers containing "ab" as well as most of the standard song forms (AABA etc.) Now you can just search for the key or keys.

### App focus, or what this app is *not*
Inspired by the Unix philosophy of "do one thing and do it well", the focus of this app is repertoire management. It is not a general practice app, and it is not a tune *learning* app. There are plenty of other apps and resources that fulfill those functions. The app assumes the user has access to the materials they need to learn a tune (recordings, sheet music, etc.) outside of the app itself. What the app offers is easy and intuitive access to all tunes known by the user based on any desired criteria.

### Tech stack
Jazztunes uses [Django](https://www.djangoproject.com/) on the back end and [htmx](https://htmx.org/) on the front end with [Bootstrap](https://getbootstrap.com/) for styling. It uses [DataTables](https://datatables.net/) for column sorting.
Jazztunes uses [Django](https://www.djangoproject.com/) on the back end and [htmx](https://htmx.org/) on the front end with [Bootstrap](https://getbootstrap.com/) for styling. The database is [PostgreSQL](https://www.postgresql.org/). It uses [DataTables](https://datatables.net/) for column sorting.

### Local installation
If you want to run jazztunes locally, you'll need at least Python 3.11. Follow the following steps:
Expand Down
20 changes: 20 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
26 changes: 26 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = "jazztunes"
copyright = "2024, Jeff Jacobson"
author = "Jeff Jacobson"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = []

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]


# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "alabaster"
html_static_path = ["_static"]
20 changes: 20 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.. jazztunes documentation master file, created by
sphinx-quickstart on Fri Jun 14 14:20:30 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to jazztunes's documentation!
=====================================

.. toctree::
:maxdepth: 2
:caption: Contents:



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
35 changes: 35 additions & 0 deletions docs/make.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)

if "%1" == "" goto help

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%

:end
popd
Binary file added latest.dump
Binary file not shown.
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ django-bootstrap5
sendgrid
python-dotenv
pytest-env
sphinx==7.3.7
156 changes: 132 additions & 24 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.contrib.messages.storage.fallback import FallbackStorage
from django.http import HttpRequest

from tune.views import query_tunes, return_search_results, exclude_term
from tune.views import query_tunes, return_search_results, exclude_term, search_field


@pytest.fixture
Expand Down Expand Up @@ -38,14 +38,12 @@ def search_form_fixture():
def test_query_tunes_kern(tune_set):
search_terms = ["kern"]
result = query_tunes(tune_set["tunes"], search_terms)
result_titles = {tune.tune.title for tune in result}
expected_titles = {"All the Things You Are", "Dearly Beloved", "Long Ago and Far Away"}

assert result.count() == 3
assert all("Kern" in tune.tune.composer for tune in result)

for title in expected_titles:
assert title in result_titles
assert all("Kern" in tune.tune.composer for tune in result) and (
tune.tune.title in expected_titles for tune in result
)


@pytest.mark.django_db
Expand All @@ -68,32 +66,29 @@ def test_query_tunes_no_results(tune_set):
def test_query_tunes_nickname(tune_set):
search_terms = ["bird"]
result = query_tunes(tune_set["tunes"], search_terms)
result_titles = {tune.tune.title for tune in result}
expected_titles = {"Confirmation", "Dewey Square"}

assert result.count() == 2

for title in expected_titles:
assert title in result_titles
for tune in result:
assert tune.tune.title in expected_titles


@pytest.mark.django_db
def test_query_tunes_common_fragment(tune_set):
search_terms = ["love"]
result = query_tunes(tune_set["tunes"], search_terms)
result_titles = {tune.tune.title for tune in result}
expected_titles = {"Dearly Beloved", "A Flower is a Lovesome Thing"}

assert result.count() == 2
for title in expected_titles:
assert title in result_titles
for tune in result:
assert tune.tune.title in expected_titles


@pytest.mark.django_db
def test_query_tunes_decade(tune_set):
search_terms = ["194"]
result = query_tunes(tune_set["tunes"], search_terms)
result_titles = {tune.tune.title for tune in result}
expected_titles = {
"Dearly Beloved",
"A Flower is a Lovesome Thing",
Expand All @@ -104,35 +99,33 @@ def test_query_tunes_decade(tune_set):
}

assert result.count() == 6
for title in expected_titles:
assert title in result_titles
for tune in result:
assert tune.tune.title in expected_titles


@pytest.mark.django_db
def test_query_tunes_form(tune_set):
search_terms = ["abac"]
result = query_tunes(tune_set["tunes"], search_terms)
result_titles = {tune.tune.title for tune in result}
expected_titles = {
"Dearly Beloved",
"Someday My Prince Will Come",
"Long Ago and Far Away",
}

assert result.count() == 3
for title in expected_titles:
assert title in result_titles
for tune in result:
assert tune.tune.title in expected_titles


@pytest.mark.django_db
def test_query_tunes_exclude(tune_set):
search_terms = ["-kern"]
result = query_tunes(tune_set["tunes"], search_terms)
result_composers = {tune.tune.composer for tune in result}

assert result.count() == 7
for composer in result_composers:
assert "kern" not in composer
for tune in result:
assert tune.tune.composer != "Kern"


# Two term tests
Expand Down Expand Up @@ -186,6 +179,9 @@ def test_query_tunes_exclude2(tune_set):
result = query_tunes(tune_set["tunes"], search_terms)

assert result.count() == 6
for tune in result:
assert tune.tune.composer != "Kern"
assert "love" not in tune.tune.title.lower()


# Timespan tests
Expand Down Expand Up @@ -235,8 +231,120 @@ def test_return_search_results_too_many(request_fixture, tune_set, search_form_f
def test_exclude_term(tune_set):
excluded_term = "-kern"
result = exclude_term(tune_set["tunes"], excluded_term)
result_composers = {tune.tune.composer for tune in result}

assert result.count() == 7
for composer in result_composers:
assert "kern" not in composer
for tune in result:
assert tune.tune.composer != "Kern"
assert "kern" not in tune.tune.title.lower()


def test_search_field_title(tune_set):
search_term = "title:you"
result = search_field(tune_set["tunes"], search_term)
expected_titles = {"All the Things You Are", "I Remember You"}

assert result.count() == 2
for tune in result:
assert tune.tune.title in expected_titles


def test_search_field_composer(tune_set):
search_term = "composer:parker"
result = search_field(tune_set["tunes"], search_term)
expected_composer = "Parker"
expected_titles = {"Confirmation", "Dewey Square"}

assert result.count() == 2
for tune in result:
assert tune.tune.composer == expected_composer
assert tune.tune.title in expected_titles


def test_search_field_key(tune_set):
search_term = "key:f"
result = search_field(tune_set["tunes"], search_term)
expected_key = "F"
expected_titles = {"Confirmation", "Long Ago and Far Away", "I Remember You"}

assert result.count() == 3
for tune in result:
assert tune.tune.key == expected_key
assert tune.tune.title in expected_titles


def test_search_field_keys(tune_set):
search_term = "keys:eb"
result = search_field(tune_set["tunes"], search_term)
expected_key = "Eb"
expected_titles = {"Dewey Square", "All the Things You Are", "Someday My Prince Will Come"}

assert result.count() == 3
for tune in result:
assert tune.tune.key == expected_key or expected_key in tune.tune.other_keys
assert tune.tune.title in expected_titles


def test_search_field_form(tune_set):
search_term = "form:abac"
result = search_field(tune_set["tunes"], search_term)
expected_form = "ABAC"
expected_titles = {"Dearly Beloved", "Long Ago and Far Away", "Someday My Prince Will Come"}

assert result.count() == 3
for tune in result:
assert tune.tune.song_form == expected_form
assert tune.tune.title in expected_titles


def test_search_field_style(tune_set):
search_term = "style:jazz"
result = search_field(tune_set["tunes"], search_term)
expected_style = "jazz"
expected_titles = {
"Confirmation",
"Dewey Square",
"Coming on the Hudson",
"Kary's Trance",
"A Flower is a Lovesome Thing",
}

assert result.count() == 5
for tune in result:
assert tune.tune.style == expected_style
assert tune.tune.title in expected_titles


def test_search_field_meter(tune_set):
search_term = "meter:3"
result = search_field(tune_set["tunes"], search_term)
expected_meter = 3
expected_titles = {"Someday My Prince Will Come"}

assert result.count() == 1
for tune in result:
assert tune.tune.meter == expected_meter
assert tune.tune.title in expected_titles


def test_search_field_year(tune_set):
search_term = "year:1941"
result = search_field(tune_set["tunes"], search_term)
expected_year = 1941
expected_titles = {"I Remember You", "A Flower is a Lovesome Thing"}

assert result.count() == 2
for tune in result:
assert tune.tune.year == expected_year
assert tune.tune.title in expected_titles


def test_search_field_year_partial(tune_set):
search_term = "year:195"
result = search_field(tune_set["tunes"], search_term)
expected_years = {1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959}
expected_titles = {"Kary's Trance", "Coming on the Hudson"}

assert result.count() == 2
for tune in result:
assert tune.tune.year in expected_years
assert tune.tune.title in expected_titles
3 changes: 3 additions & 0 deletions tune/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,6 @@ class Meta:

def __str__(self):
return f"{self.tune} | {self.player}"


# class Plays(models.Model):

0 comments on commit 5378fc0

Please sign in to comment.