diff --git a/app_run/__init__.py b/app_run/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app_run/admin.py b/app_run/admin.py new file mode 100644 index 00000000..bd39f3fb --- /dev/null +++ b/app_run/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from app_run.models import Run + + +@admin.register(Run) +class RunAdmin(admin.ModelAdmin): + list_display = ('athlete', 'created_at', 'comment') + list_filter = ('status',) diff --git a/app_run/apps.py b/app_run/apps.py new file mode 100644 index 00000000..660c9321 --- /dev/null +++ b/app_run/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppRunConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app_run' diff --git a/app_run/migrations/0001_initial.py b/app_run/migrations/0001_initial.py new file mode 100644 index 00000000..cb564490 --- /dev/null +++ b/app_run/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2 on 2025-07-05 16:29 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Run', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('comment', models.TextField(blank=True)), + ('athlete', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/app_run/migrations/0002_run_status.py b/app_run/migrations/0002_run_status.py new file mode 100644 index 00000000..7d8c5e22 --- /dev/null +++ b/app_run/migrations/0002_run_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2 on 2025-07-10 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app_run', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='run', + name='status', + field=models.CharField(choices=[('init', 'Init'), ('in_progress', 'In Progress'), ('finished', 'Finished')], default='init', max_length=11), + ), + ] diff --git a/app_run/migrations/__init__.py b/app_run/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app_run/models.py b/app_run/models.py new file mode 100644 index 00000000..942d3b82 --- /dev/null +++ b/app_run/models.py @@ -0,0 +1,20 @@ +from django.db import models + + +class Run(models.Model): + class RunStatus(models.TextChoices): + INIT = 'init' + IN_PROGRESS = 'in_progress' + FINISHED = 'finished' + + athlete = models.ForeignKey('auth.User', on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + comment = models.TextField(blank=True) + status = models.CharField( + max_length=11, + choices=RunStatus, + default=RunStatus.INIT + ) + + def __str__(self) -> str: + return f"Run by {self.athlete.username} on {self.created_at.date()}" diff --git a/app_run/serializers.py b/app_run/serializers.py new file mode 100644 index 00000000..a82fe8f1 --- /dev/null +++ b/app_run/serializers.py @@ -0,0 +1,31 @@ +from django.contrib.auth.models import User + +from rest_framework import serializers + +from app_run.models import Run + +class RunUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'first_name', 'last_name'] + + +class RunSerializer(serializers.ModelSerializer): + athlete_data = RunUserSerializer(source='athlete', read_only=True) + + class Meta: + model = Run + fields = '__all__' + + +class UserSerializer(serializers.ModelSerializer): + type = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ['id', 'username', 'date_joined', 'first_name', 'last_name', 'type'] + + def get_type(self, obj): + if obj.is_staff: + return 'coach' + return 'athlete' diff --git a/app_run/tests.py b/app_run/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/app_run/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app_run/views.py b/app_run/views.py new file mode 100644 index 00000000..75c1a36d --- /dev/null +++ b/app_run/views.py @@ -0,0 +1,91 @@ +from rest_framework.decorators import api_view +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.filters import SearchFilter +from rest_framework import status +from django.conf import settings +from django.contrib.auth.models import User +from django.db.models import QuerySet +from django.shortcuts import get_object_or_404 + +from app_run.models import Run +from app_run.serializers import RunSerializer, UserSerializer + +@api_view(['GET']) +def company_details(request): + details = { + 'company_name': settings.COMPANY_NAME, + 'slogan': settings.SLOGAN, + 'contacts': settings.CONTACTS + } + return Response(details) + + +class RunViewSet(ModelViewSet): + queryset = Run.objects.select_related('athlete').all() + serializer_class = RunSerializer + filter_backends = [SearchFilter] + search_fields = ['first_name', 'last_name'] + + +class UserViewSet(ReadOnlyModelViewSet): + """ + ViewSet for retrieving user information. + + This viewset provides 'list' and 'retrieve' actions for User objects, + excluding superusers. It supports filtering by user type ('coach' or 'athlete') + via query parameters. + """ + serializer_class = UserSerializer + queryset = User.objects.filter(is_superuser=False) + lookup_field = 'is_staff' + filter_backends = [SearchFilter] + search_fields = ['first_name', 'last_name'] + + def get_queryset(self) -> 'QuerySet[User]': + """ + Filter the queryset based on the 'type' query parameter. + + Returns: + QuerySet[User]: Filtered queryset of User objects. + - If type='coach': returns staff users + - If type='athlete': returns non-staff users + - Otherwise: returns all non-superuser users + """ + user_type = self.request.query_params.get('type') + + # Use a dictionary mapping for cleaner, more maintainable code + type_filters = { + 'coach': {'is_staff': True}, + 'athlete': {'is_staff': False} + } + + if user_type in type_filters: + return self.queryset.filter(**type_filters[user_type]) + + return self.queryset + + +class RunStartAPIView(APIView): + def put(self, request, pk, format=None): + run = get_object_or_404(Run, pk=pk) + if run.status not in [Run.RunStatus.IN_PROGRESS, Run.RunStatus.FINISHED]: + serializer = RunSerializer(run, data={'status': Run.RunStatus.IN_PROGRESS}, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response({'message': 'Run already started'}, status=status.HTTP_400_BAD_REQUEST) + + +class RunStopAPIView(APIView): + def put(self, request, pk, format=None): + run = get_object_or_404(Run, pk=pk) + if run.status == Run.RunStatus.IN_PROGRESS: + serializer = RunSerializer(run, data={'status': Run.RunStatus.FINISHED}, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response({'message': 'Run not started'}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/project_run/settings/base.py b/project_run/settings/base.py index fac33f1f..0ac13f35 100644 --- a/project_run/settings/base.py +++ b/project_run/settings/base.py @@ -37,6 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'app_run', ] MIDDLEWARE = [ @@ -110,3 +112,7 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +COMPANY_NAME = 'Running Skuf' +SLOGAN = 'Run, Skuf, Run' +CONTACTS = '@pan_ded' diff --git a/project_run/settings/local.py b/project_run/settings/local.py index 128ab27b..ec340413 100644 --- a/project_run/settings/local.py +++ b/project_run/settings/local.py @@ -8,4 +8,21 @@ 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } +} + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + }, + }, + "loggers": { + "django.db.backends": { + "handlers": ["console"], + "level": "DEBUG", + }, + }, } \ No newline at end of file diff --git a/project_run/urls.py b/project_run/urls.py index acda9f53..cdb1634b 100644 --- a/project_run/urls.py +++ b/project_run/urls.py @@ -16,9 +16,15 @@ """ from django.contrib import admin from django.urls import path -from django.conf.urls.static import static -from django.conf import settings + +from app_run.views import RunStartAPIView, RunStopAPIView, company_details, UserViewSet, RunViewSet urlpatterns = [ path('admin/', admin.site.urls), + path('api/company_details/', company_details), + path('api/runs/', RunViewSet.as_view({'get': 'list', 'post': 'create'})), + path('api/runs//', RunViewSet.as_view({'get': 'retrieve'})), + path('api/users/', UserViewSet.as_view({'get': 'list'})), + path('api/runs//start/', RunStartAPIView.as_view()), + path('api/runs//stop/', RunStopAPIView.as_view()), ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f1b0b31f..dc468b54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Django==5.2 +djangorestframework==3.16.0 psycopg2-binary==2.9.10 django-storages==1.14.6 boto3==1.37.37