diff --git a/.github/workflows/blackformatter.yml b/.github/workflows/blackformatter.yml new file mode 100644 index 0000000..05bd5f0 --- /dev/null +++ b/.github/workflows/blackformatter.yml @@ -0,0 +1,27 @@ +name: Check Python Code Formatting + +on: + pull_request: + branches: + - main + +jobs: + format-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12.x" + + - name: Install Black + run: pip install black + + - name: Run Black + run: black . --check diff --git a/README.md b/README.md index d81bf9c..3c286a6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TeleBand +# MusicCPR The music education learning management system diff --git a/config/api_router.py b/config/api_router.py index 67e7765..4e6d6fc 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -20,6 +20,7 @@ ) from teleband.musics.api.views import PieceViewSet from teleband.instruments.api.views import InstrumentViewSet +from teleband.users.api.views import UserInstrumentConfigViewSet if settings.DEBUG: router = DefaultRouter() @@ -34,6 +35,7 @@ router.register("pieces", PieceViewSet) router.register("piece-plans", PiecePlanViewSet) router.register("instruments", InstrumentViewSet) +router.register("configs", UserInstrumentConfigViewSet) courses_router = nested_cls(router, "courses", lookup="course_slug") courses_router.register("assignments", AssignmentViewSet) # option basename omitted @@ -51,6 +53,9 @@ attachments_router = nested_cls(assignments_router, "submissions", lookup="submission") attachments_router.register("attachments", AttachmentViewSet) +# config_router = nested_cls(router, "users", lookup="user_username") +# config_router.register("configs", UserInstrumentConfigViewSet) + app_name = "api" urlpatterns = router.urls urlpatterns += [ diff --git a/docs/api.md b/docs/api.md index 8cac8c7..8e8f89f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -55,8 +55,8 @@ Remove a user/role/course enrollment curl -v \ --request GET \ --header 'Content-Type: application/json' \ --H 'Authorization: Token e9a82a7c334fbdfc52f502efebebec474708eef0' \ -https://dev-api.tele.band/api/pieces/ && echo "\n" +-H 'Authorization: Token a2ffdae8df89b2909eb03d21cec559e95eba2e44' \ +http://127.0.0.1:8000/api/configs/ && echo "\n" ``` diff --git a/teleband/instruments/migrations/0005_instrumentconfig.py b/teleband/instruments/migrations/0005_instrumentconfig.py new file mode 100644 index 0000000..3043df9 --- /dev/null +++ b/teleband/instruments/migrations/0005_instrumentconfig.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.6 on 2025-11-13 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("instruments", "0004_instrument_midi_program_number"), + ] + + operations = [ + migrations.CreateModel( + name="InstrumentConfig", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=20)), + ("description", models.CharField(max_length=100)), + ("settings", models.JSONField(default=dict)), + ], + ), + ] diff --git a/teleband/instruments/migrations/0006_delete_instrumentconfig.py b/teleband/instruments/migrations/0006_delete_instrumentconfig.py new file mode 100644 index 0000000..7163067 --- /dev/null +++ b/teleband/instruments/migrations/0006_delete_instrumentconfig.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.6 on 2025-11-13 17:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("instruments", "0005_instrumentconfig"), + ] + + operations = [ + migrations.DeleteModel( + name="InstrumentConfig", + ), + ] diff --git a/teleband/users/admin.py b/teleband/users/admin.py index 1112a8d..956fe9a 100644 --- a/teleband/users/admin.py +++ b/teleband/users/admin.py @@ -8,7 +8,7 @@ from teleband.users.forms import UserChangeForm, UserCreationForm from teleband.users.models import Role, GroupInvitation from teleband.courses.models import Enrollment - +from teleband.users.models import InstrumentConfig User = get_user_model() @@ -51,3 +51,5 @@ class UserAdmin(auth_admin.UserAdmin): class RoleAdmin(VersionAdmin): list_display = ("id", "name") search_fields = ("name",) + +admin.site.register(InstrumentConfig) \ No newline at end of file diff --git a/teleband/users/api/serializers.py b/teleband/users/api/serializers.py index e3379a4..654c952 100644 --- a/teleband/users/api/serializers.py +++ b/teleband/users/api/serializers.py @@ -4,6 +4,7 @@ from teleband.instruments.api.serializers import InstrumentSerializer from teleband.utils.serializers import GenericNameSerializer +from teleband.users.models import InstrumentConfig User = get_user_model() @@ -35,3 +36,9 @@ class UserInstrumentSerializer(serializers.ModelSerializer): class Meta: model = User fields = ["id", "name", "instrument", "external_id", "grade"] + + +class UserInstrumentConfigSerializer(serializers.ModelSerializer): + class Meta: + model = InstrumentConfig + fields = ["id", "name", "description", "settings", "file"] diff --git a/teleband/users/api/views.py b/teleband/users/api/views.py index f47be9f..e4a0f6f 100644 --- a/teleband/users/api/views.py +++ b/teleband/users/api/views.py @@ -13,7 +13,7 @@ from rest_framework.decorators import action from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import ObtainAuthToken @@ -22,9 +22,15 @@ from invitations.exceptions import AlreadyAccepted, AlreadyInvited, UserRegisteredEmail from invitations.forms import CleanEmailMixin -from .serializers import UserSerializer, UserInstrumentSerializer +from .serializers import ( + UserSerializer, + UserInstrumentSerializer, + UserInstrumentConfigSerializer, +) from teleband.courses.models import Enrollment, Course +from teleband.users.models import InstrumentConfig +from django.db.models import Q User = get_user_model() Invitation = get_invitation_model() @@ -140,4 +146,17 @@ def post(self, request, *args, **kwargs): return response +class UserInstrumentConfigViewSet(ModelViewSet): + serializer_class = UserInstrumentConfigSerializer + queryset = InstrumentConfig.objects.all() + + def get_queryset(self): + # this returns all configs for the user and the default confgis (those with user=None) + return InstrumentConfig.objects.filter(Q(user=self.request.user) | Q(user=None)) + + # this helped to map the configs to the user creating them + def perform_create(self, serializer): + return serializer.save(user=self.request.user) + + obtain_delete_auth_token = ObtainDeleteAuthToken.as_view() diff --git a/teleband/users/migrations/0011_instrumentconfig.py b/teleband/users/migrations/0011_instrumentconfig.py new file mode 100644 index 0000000..184ce8a --- /dev/null +++ b/teleband/users/migrations/0011_instrumentconfig.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.6 on 2025-11-13 17:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0010_alter_user_external_id"), + ] + + operations = [ + migrations.CreateModel( + name="InstrumentConfig", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=20)), + ("description", models.CharField(max_length=100)), + ("settings", models.JSONField(default=dict)), + ], + ), + ] diff --git a/teleband/users/migrations/0012_instrumentconfig_user.py b/teleband/users/migrations/0012_instrumentconfig_user.py new file mode 100644 index 0000000..0e7984d --- /dev/null +++ b/teleband/users/migrations/0012_instrumentconfig_user.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2025-11-20 14:21 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0011_instrumentconfig"), + ] + + operations = [ + migrations.AddField( + model_name="instrumentconfig", + name="user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/teleband/users/migrations/0013_instrumentconfig_file.py b/teleband/users/migrations/0013_instrumentconfig_file.py new file mode 100644 index 0000000..05bcf44 --- /dev/null +++ b/teleband/users/migrations/0013_instrumentconfig_file.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.6 on 2026-03-30 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0012_instrumentconfig_user"), + ] + + operations = [ + migrations.AddField( + model_name="instrumentconfig", + name="file", + field=models.FileField( + blank=True, null=True, upload_to="instrument_config_samples/" + ), + ), + ] diff --git a/teleband/users/migrations/0014_alter_instrumentconfig_description.py b/teleband/users/migrations/0014_alter_instrumentconfig_description.py new file mode 100644 index 0000000..86a55c9 --- /dev/null +++ b/teleband/users/migrations/0014_alter_instrumentconfig_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2026-04-20 16:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0013_instrumentconfig_file"), + ] + + operations = [ + migrations.AlterField( + model_name="instrumentconfig", + name="description", + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/teleband/users/migrations/0015_auto_20260420_1659.py b/teleband/users/migrations/0015_auto_20260420_1659.py new file mode 100644 index 0000000..7377fdf --- /dev/null +++ b/teleband/users/migrations/0015_auto_20260420_1659.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2026-04-20 20:59 + +from django.db import migrations +defaultConfigs = [ + { + "name": "Roland-GR-1-Trumpet", + "settings": {}, + "file": "instrument_config_samples/Roland-GR-1-Trumpet-C5.wav", + "user": None + } + ] + +def update_site_forward(apps, schema_editor): + instrument_config_model = apps.get_model("users", "InstrumentConfig") + for config in defaultConfigs: + instrument_config_model.objects.create(**config) + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0014_alter_instrumentconfig_description"), + ] + + operations = [migrations.RunPython(update_site_forward)] diff --git a/teleband/users/migrations/0016_update_default_config_description.py b/teleband/users/migrations/0016_update_default_config_description.py new file mode 100644 index 0000000..818a602 --- /dev/null +++ b/teleband/users/migrations/0016_update_default_config_description.py @@ -0,0 +1,14 @@ +# Generated by Django 5.0.6 on 2026-04-21 01:21 + +from django.db import migrations + +def update_site_forward(apps, schema_editor): + instrument_config_model = apps.get_model("users", "InstrumentConfig") + instrument_config_model.objects.filter(name="Roland-GR-1-Trumpet", user=None).update(description="Default Roland GR-1 Trumpet configuration.") +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0015_auto_20260420_1659"), + ] + + operations = [migrations.RunPython(update_site_forward)] diff --git a/teleband/users/models.py b/teleband/users/models.py index acb438d..9597523 100644 --- a/teleband/users/models.py +++ b/teleband/users/models.py @@ -48,3 +48,14 @@ def __str__(self): class GroupInvitation(Invitation): group = models.ForeignKey(Group, on_delete=models.DO_NOTHING) + + +class InstrumentConfig(models.Model): + name = models.CharField(max_length=20) + description = models.CharField(max_length=100, null=True, blank=True) + settings = models.JSONField(default=dict) + file = models.FileField(upload_to="instrument_config_samples/", null=True, blank=True) + user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) + + def __str__(self): + return self.name