From c5597eb4c490568283bb5cc893bf890def756dc3 Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Sun, 23 Jun 2024 19:20:37 -0400 Subject: [PATCH 1/8] Implement tagging (incomplete) --- tune/admin.py | 10 ++-- tune/forms.py | 10 +++- .../0005_tag_repertoiretune_tags.py | 34 ++++++++++++++ tune/models.py | 10 ++-- tune/templates/tune/list.html | 4 +- tune/views.py | 47 ++++++++++++++----- 6 files changed, 89 insertions(+), 26 deletions(-) create mode 100644 tune/migrations/0005_tag_repertoiretune_tags.py diff --git a/tune/admin.py b/tune/admin.py index e57090f..c77498b 100644 --- a/tune/admin.py +++ b/tune/admin.py @@ -17,7 +17,7 @@ from django.contrib import admin -from tune.models import Tune, RepertoireTune +from tune.models import Tune, RepertoireTune, Tag @admin.register(Tune) @@ -34,7 +34,7 @@ class RepertoireTuneAdmin(admin.ModelAdmin): autocomplete_fields = ("tune", "player") -# @admin.register(Tag) -# class TagAdmin(admin.ModelAdmin): -# list_display = ["name"] -# search_fields = ("name",) +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + list_display = ["name"] + search_fields = ("name",) diff --git a/tune/forms.py b/tune/forms.py index 84955c6..27d4ca1 100644 --- a/tune/forms.py +++ b/tune/forms.py @@ -91,13 +91,21 @@ class RepertoireTuneForm(ModelForm): class Meta: model = RepertoireTune exclude = ["tune", "player", "started_learning", "play_count"] - widgets = {"last_played": DateInput()} + widgets = {"last_played": DateInput(), "tags": forms.SelectMultiple()} def __init__(self, *args, **kwargs): super(RepertoireTuneForm, self).__init__(*args, **kwargs) for field in self.fields: self.fields[field].widget.attrs["class"] = "form-control" + def save(self, commit=True): + instance = super(RepertoireTuneForm, self).save(commit=False) + if commit: + instance.save() + self.save_m2m() + # breakpoint() + return instance + class SearchForm(forms.Form): TIMES = [ diff --git a/tune/migrations/0005_tag_repertoiretune_tags.py b/tune/migrations/0005_tag_repertoiretune_tags.py new file mode 100644 index 0000000..6c8d2d9 --- /dev/null +++ b/tune/migrations/0005_tag_repertoiretune_tags.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.4 on 2024-06-22 20:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tune", "0004_alter_repertoiretune_last_played"), + ] + + operations = [ + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=20, unique=True)), + ], + ), + migrations.AddField( + model_name="repertoiretune", + name="tags", + field=models.ManyToManyField( + related_name="repertoire_tunes", to="tune.tag" + ), + ), + ] diff --git a/tune/models.py b/tune/models.py index 80a2e2c..962b015 100644 --- a/tune/models.py +++ b/tune/models.py @@ -19,13 +19,6 @@ from django.contrib.auth import get_user_model -# class Tag(models.Model): -# name = models.CharField(max_length=20) - -# def __str__(self): -# return self.name - - class Tune(models.Model): """ The Tune is the heart of this app; each tune is one song that can be added to a user's repertoire and should contain all relevant musical information. @@ -147,6 +140,9 @@ def __str__(self): class Tag(models.Model): name = models.CharField(max_length=20, unique=True) + def __str__(self): + return self.name + class RepertoireTune(models.Model): """ diff --git a/tune/templates/tune/list.html b/tune/templates/tune/list.html index afbdc1f..2e08def 100644 --- a/tune/templates/tune/list.html +++ b/tune/templates/tune/list.html @@ -69,7 +69,7 @@

Style Meter Year - + Tags Knowledge Last played Actions @@ -86,7 +86,7 @@

{{ tune.tune.style }} {{ tune.tune.meter }} {{ tune.tune.year }} - + {{ tune.tags.name }} {{ tune.knowledge }} {{ tune.last_played|date:"MONTH_DAY_FORMAT"}} diff --git a/tune/views.py b/tune/views.py index 377cc52..7e5cede 100644 --- a/tune/views.py +++ b/tune/views.py @@ -80,7 +80,11 @@ def query_tunes(tune_set, search_terms, timespan=None): if term and term[0] == "-": term_query = exclude_term(tune_set, term) - elif term and len(term.split(":")) > 1 and term.split(":")[0].lower() in Tune.field_names: + elif ( + term + and len(term.split(":")) > 1 + and term.split(":")[0].lower() in Tune.field_names + ): term_query = search_field(tune_set, term) else: @@ -97,7 +101,9 @@ def query_tunes(tune_set, search_terms, timespan=None): ) if term in Tune.NICKNAMES: - nickname_query = tune_set.filter(Q(tune__composer__icontains=Tune.NICKNAMES[term])) + nickname_query = tune_set.filter( + Q(tune__composer__icontains=Tune.NICKNAMES[term]) + ) term_query = term_query | nickname_query if timespan is not None: @@ -158,7 +164,9 @@ def tune_list(request): search_terms = search_form.cleaned_data["search_term"].split(" ") search_term_string = " ".join(search_terms) timespan = search_form.cleaned_data["timespan"] - results = return_search_results(request, search_terms, tunes, search_form, timespan) + results = return_search_results( + request, search_terms, tunes, search_form, timespan + ) tunes = results.get("tunes") tune_count = results.get("tune_count", 0) else: @@ -185,13 +193,17 @@ def tune_new(request): if request.method != "POST": tune_form = TuneForm() rep_form = RepertoireTuneForm() - return render(request, "tune/form.html", {"tune_form": tune_form, "rep_form": rep_form}) + return render( + request, "tune/form.html", {"tune_form": tune_form, "rep_form": rep_form} + ) tune_form = TuneForm(request.POST) rep_form = RepertoireTuneForm(request.POST) if not tune_form.is_valid() or not rep_form.is_valid(): - return render(request, "tune/form.html", {"tune_form": tune_form, "rep_form": rep_form}) + return render( + request, "tune/form.html", {"tune_form": tune_form, "rep_form": rep_form} + ) with transaction.atomic(): new_tune = tune_form.save(commit=False) @@ -225,21 +237,28 @@ def tune_edit(request, pk): tune_form = TuneForm(request.POST or None, instance=tune) rep_form = RepertoireTuneForm(request.POST or None, instance=rep_tune) - # breakpoint() + if tune_form.is_valid() and rep_form.is_valid(): with transaction.atomic(): updated_tune = tune_form.save() - rep_form.save() + updated_rep_tune = rep_form.save() + print(f"Before: {updated_rep_tune.tags.all()}") messages.success( request, f"{updated_tune.title} has been updated.", ) + print(f"After: {updated_rep_tune.tags.all()}") return redirect("tune:tune_list") return render( request, "tune/form.html", - {"tune": tune, "rep_tune": rep_tune, "tune_form": tune_form, "rep_form": rep_form}, + { + "tune": tune, + "rep_tune": rep_tune, + "tune_form": tune_form, + "rep_form": rep_form, + }, ) @@ -289,7 +308,9 @@ def get_random_tune(request): search_terms = search_form.cleaned_data["search_term"].split(" ") timespan = search_form.cleaned_data.get("timespan", None) - result_dict = return_search_results(request, search_terms, tunes, search_form, timespan) + result_dict = return_search_results( + request, search_terms, tunes, search_form, timespan + ) if "error" in result_dict: messages.error(request, result_dict["error"]) @@ -368,7 +389,9 @@ def tune_browse(request): View for loading the public page, where users can browse public tunes and take them into their repertoire """ - user_tunes = RepertoireTune.objects.select_related("tune").filter(player=request.user) + user_tunes = RepertoireTune.objects.select_related("tune").filter( + player=request.user + ) user_tune_titles = {tune.tune.title for tune in user_tunes} tunes = RepertoireTune.objects.select_related("tune").filter( @@ -380,7 +403,9 @@ def tune_browse(request): search_form = SearchForm(request.POST) if search_form.is_valid(): search_terms = search_form.cleaned_data["search_term"].split(" ") - tunes = return_search_results(request, search_terms, tunes, search_form)["tunes"] + tunes = return_search_results(request, search_terms, tunes, search_form)[ + "tunes" + ] tune_count = len(tunes) else: search_form = SearchForm() From d74f0a4d9b6a6fef23c287c55cfb8e760e8cb09a Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Sun, 23 Jun 2024 19:23:41 -0400 Subject: [PATCH 2/8] Add env-template --- env-template | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 env-template diff --git a/env-template b/env-template new file mode 100644 index 0000000..1564f66 --- /dev/null +++ b/env-template @@ -0,0 +1,9 @@ +DATABASE_URL= +ADMIN_USER_ID= +SECRET_KEY= +DEBUG= +ALLOWED_HOSTS= +SENTRY_DSN= +SENDGRID_API_KEY= +DEFAULT_FROM_EMAIL= +PYTHONBREAKPOINT=ipdb.set_trace \ No newline at end of file From 01ae42b9a2edfe32e6d58ace5de1e36ddf016aca Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:01:59 -0400 Subject: [PATCH 3/8] Update list template to display tags --- tune/templates/tune/list.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tune/templates/tune/list.html b/tune/templates/tune/list.html index 2e08def..2c122c7 100644 --- a/tune/templates/tune/list.html +++ b/tune/templates/tune/list.html @@ -86,7 +86,11 @@

{{ tune.tune.style }} {{ tune.tune.meter }} {{ tune.tune.year }} - {{ tune.tags.name }} + + {% for tag in tune.tags.all %} +
{{ tag.name }}
+ {% endfor %} + {{ tune.knowledge }} {{ tune.last_played|date:"MONTH_DAY_FORMAT"}} From 563af78230eb2d7655d18a60c037be5511f6b54d Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:30:30 -0400 Subject: [PATCH 4/8] Update readme --- README.md | 10 ++++++---- env-template | 17 ++++++++++++----- tune/templates/tune/form.html | 5 ++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 64742da..28eb510 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,17 @@ This readme focuses on technical aspects of the app of interest to developers; f Jazztunes uses [Django 5.0](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. Tests are written in [pytest](https://docs.pytest.org/en/8.2.x/). It runs on Python 3.11 or later. ### Local installation -If you want to run jazztunes locally, follow the following steps: +If you want to run jazztunes locally, take the following steps: 1. Clone this repository. 2. Navigate to the 'jazztunes' directory. 3. Create a virtual environment ```python -m venv venv''' (Windows/Linux) or ```python3 -m venv venv``` (Mac). 4. Activate the virtual environment ```.\venv\Scripts\activate``` (Windows) or ```source venv/bin/activate``` 5. Install the necessary packages: ```pip install -r requirements.txt``` -6. Run the program: ```python manage.py runserver ``` -7. Ctrl-click on ```http://127.0.0.1:8000``` — This will open jazztunes in your default browser. -8. You can close the program by closing your browser and pressing Ctrl-C in the terminal running it. +6. Create a .env file in the root directory with the variables (I've supplied a file, env-template, that shows what you need and has some default values) +7. If you want to use the Public tune feature, you'll need to create a superuser: ```python manage.py createsuperuser```, then set that user's ID to ADMIN_USER_ID in .env (it will be 1 if it's the first user created). Then tunes you create as that user will also show up on the Public page. Creating a superuser is also a good idea because it gives you access to the [Django admin](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/) interface. +8. Run the program: ```python manage.py runserver ``` +9. Ctrl-click on ```http://127.0.0.1:8000``` — This will open jazztunes in your default browser. +10. You can close the program by closing your browser and pressing Ctrl-C in the terminal running it. ### License Jazztunes is [free software](https://www.fsf.org/about/what-is-free-software), released under version 3.0 of the GPL. Everyone has the right to use, modify, and distribute jazztunes subject to the [stipulations](https://github.com/jwjacobson/jazztunes/blob/main/LICENSE) of that license. diff --git a/env-template b/env-template index 1564f66..e9e62b9 100644 --- a/env-template +++ b/env-template @@ -1,8 +1,15 @@ -DATABASE_URL= -ADMIN_USER_ID= -SECRET_KEY= -DEBUG= -ALLOWED_HOSTS= +""" +This file contains the environment variables you'll need to set in .env for the app to run. +I've supplied some default values that might work well. +""" +# The following are required: +DATABASE_URL=sqlite:///db.sqlite3 +ADMIN_USER_ID=1 +SECRET_KEY='whatever' +DEBUG=True +ALLOWED_HOSTS=127.0.0.1 + +# These are optional: SENTRY_DSN= SENDGRID_API_KEY= DEFAULT_FROM_EMAIL= diff --git a/tune/templates/tune/form.html b/tune/templates/tune/form.html index 197f629..0d3010a 100644 --- a/tune/templates/tune/form.html +++ b/tune/templates/tune/form.html @@ -13,7 +13,7 @@

{% if tune %}Edit{% else %}New{% endif %} Tune --> -
+
@@ -42,6 +42,9 @@

{% if tune %}Edit{% else %}New{% endif %} Tune

+ {% if forloop.counter|divisibleby:2 %} +
+ {% endif %} {% endfor %}
From 4b1eba3cc2a09d1caecacd851c90fe7944fbe804 Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:43:41 -0400 Subject: [PATCH 5/8] Set blank=True on tags --- README.md | 2 +- tune/forms.py | 10 ---------- .../0006_alter_repertoiretune_tags.py | 19 +++++++++++++++++++ tune/models.py | 2 +- 4 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 tune/migrations/0006_alter_repertoiretune_tags.py diff --git a/README.md b/README.md index 28eb510..92f4da0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ If you want to run jazztunes locally, take the following steps: 4. Activate the virtual environment ```.\venv\Scripts\activate``` (Windows) or ```source venv/bin/activate``` 5. Install the necessary packages: ```pip install -r requirements.txt``` 6. Create a .env file in the root directory with the variables (I've supplied a file, env-template, that shows what you need and has some default values) -7. If you want to use the Public tune feature, you'll need to create a superuser: ```python manage.py createsuperuser```, then set that user's ID to ADMIN_USER_ID in .env (it will be 1 if it's the first user created). Then tunes you create as that user will also show up on the Public page. Creating a superuser is also a good idea because it gives you access to the [Django admin](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/) interface. +7. If you want to use the Public tune feature, you'll need to create a superuser: ```python manage.py createsuperuser```, then set that user's ID to ADMIN_USER_ID in .env (it will be 1 if it's the first user created, otherwise 2, etc.). Then tunes you create as that user will also show up on the Public page. Creating a superuser is also a good idea because it gives you access to the [Django admin](https://docs.djangoproject.com/en/5.0/ref/contrib/admin/) interface. 8. Run the program: ```python manage.py runserver ``` 9. Ctrl-click on ```http://127.0.0.1:8000``` — This will open jazztunes in your default browser. 10. You can close the program by closing your browser and pressing Ctrl-C in the terminal running it. diff --git a/tune/forms.py b/tune/forms.py index 27d4ca1..65683b2 100644 --- a/tune/forms.py +++ b/tune/forms.py @@ -72,16 +72,6 @@ def clean_other_keys(self): data = " ".join(formatted_data) return data - # def clean_tags(self): - # data = self.cleaned_data["tags"] - # if data is None: - # return data - # formatted_data = [] - # for tag in data.split(): - # formatted_data.append(tag.lower()) - # data = " ".join(formatted_data) - # return data - class DateInput(forms.DateInput): input_type = "date" diff --git a/tune/migrations/0006_alter_repertoiretune_tags.py b/tune/migrations/0006_alter_repertoiretune_tags.py new file mode 100644 index 0000000..98e3cf4 --- /dev/null +++ b/tune/migrations/0006_alter_repertoiretune_tags.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.4 on 2024-06-26 17:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tune", "0005_tag_repertoiretune_tags"), + ] + + operations = [ + migrations.AlterField( + model_name="repertoiretune", + name="tags", + field=models.ManyToManyField( + blank=True, related_name="repertoire_tunes", to="tune.tag" + ), + ), + ] diff --git a/tune/models.py b/tune/models.py index 962b015..debb59c 100644 --- a/tune/models.py +++ b/tune/models.py @@ -165,7 +165,7 @@ class RepertoireTune(models.Model): ) started_learning = models.DateTimeField(blank=True, null=True) play_count = models.IntegerField(default=0) - tags = models.ManyToManyField(Tag, related_name="repertoire_tunes") + tags = models.ManyToManyField(Tag, related_name="repertoire_tunes", blank=True) class Meta: unique_together = ("tune", "player") From 13c856c2f3c74506db582f67590a627d98770261 Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:44:58 -0400 Subject: [PATCH 6/8] Set meter and year to render as empty strings if None --- tune/templates/tune/list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tune/templates/tune/list.html b/tune/templates/tune/list.html index 2c122c7..b60fa12 100644 --- a/tune/templates/tune/list.html +++ b/tune/templates/tune/list.html @@ -84,8 +84,8 @@

{{ tune.tune.other_keys }} {{ tune.tune.song_form }} {{ tune.tune.style }} - {{ tune.tune.meter }} - {{ tune.tune.year }} + {{ tune.tune.meter|default_if_none:"" }} + {{ tune.tune.year|default_if_none:"" }} {% for tag in tune.tags.all %}
{{ tag.name }}
From 3f505426e32f55a1ca177b5fdfb1afb26d8a7b07 Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:56:30 -0400 Subject: [PATCH 7/8] Add help text to tags --- .../0007_alter_repertoiretune_tags.py | 22 +++++++++++++++++++ tune/models.py | 7 +++++- tune/templates/tune/form.html | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tune/migrations/0007_alter_repertoiretune_tags.py diff --git a/tune/migrations/0007_alter_repertoiretune_tags.py b/tune/migrations/0007_alter_repertoiretune_tags.py new file mode 100644 index 0000000..6b3e141 --- /dev/null +++ b/tune/migrations/0007_alter_repertoiretune_tags.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.4 on 2024-06-26 17:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tune", "0006_alter_repertoiretune_tags"), + ] + + operations = [ + migrations.AlterField( + model_name="repertoiretune", + name="tags", + field=models.ManyToManyField( + blank=True, + help_text="ctrl-click to select more than one", + related_name="repertoire_tunes", + to="tune.tag", + ), + ), + ] diff --git a/tune/models.py b/tune/models.py index debb59c..1330a9f 100644 --- a/tune/models.py +++ b/tune/models.py @@ -165,7 +165,12 @@ class RepertoireTune(models.Model): ) started_learning = models.DateTimeField(blank=True, null=True) play_count = models.IntegerField(default=0) - tags = models.ManyToManyField(Tag, related_name="repertoire_tunes", blank=True) + tags = models.ManyToManyField( + Tag, + related_name="repertoire_tunes", + blank=True, + help_text="ctrl-click to select more than one", + ) class Meta: unique_together = ("tune", "player") diff --git a/tune/templates/tune/form.html b/tune/templates/tune/form.html index 0d3010a..2c21670 100644 --- a/tune/templates/tune/form.html +++ b/tune/templates/tune/form.html @@ -40,6 +40,9 @@

{% if tune %}Edit{% else %}New{% endif %} Tune{{ field.label_tag }}

{{ field }} {{ field.errors }} + {% if field.help_text %} +

{{ field.help_text|safe }}

+ {% endif %}

{% if forloop.counter|divisibleby:2 %} From 635cefc742d036f47ff6597319653a1e22c26611 Mon Sep 17 00:00:00 2001 From: JW Jacobson <116485484+jwjacobson@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:09:52 -0400 Subject: [PATCH 8/8] Add django.yml --- .github/workflows/django.yml | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/django.yml diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000..c4085fc --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,56 @@ +name: jazztunes CI and Deployment + +on: + push: + branches: [ "main" ] + +jobs: + build-and-deploy: + environment: jazztunes + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.11, 3.12] + + # Define service containers for running tests + services: + postgres: + image: postgres + env: + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + POSTGRES_DB: testdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Tests + env: + DATABASE_URL: postgres://testuser:testpass@localhost:5432/testdb + ADMIN_USER_ID: ${{ secrets.ADMIN_USER_ID }} + ALLOWED_HOSTS: ${{ secrets.ALLOWED_HOSTS }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + + run: | + pytest + - name: Deploy to Heroku + env: + HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} + run: | + git fetch --unshallow || true + git push https://heroku:$HEROKU_API_KEY@git.heroku.com/${{ secrets.HEROKU_APP_NAME }}.git HEAD:main -f \ No newline at end of file