diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index d26eca9..2cc8d6a 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -7,13 +7,24 @@ on: branches: ["master"] workflow_dispatch: +permissions: + contents: read + pull-requests: write + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest timeout-minutes: 20 + strategy: + fail-fast: false matrix: - python-version: [3.10.5, 3.11.2, 3.11.7, 3.12.3, 3.12.7] + python-version: [3.10.4, 3.12.10, 3.13.3] services: postgres: @@ -33,44 +44,38 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: pip + + - name: Cache pip packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pip-tools - if [ ! -f requirements.txt ]; then - echo "Generating requirements.txt..." - echo "django>=5.0,<6.0" >> requirements.in - echo "djangorestframework>=3.12.0,<4.0" >> requirements.in - echo "psycopg2>=2.9.0" >> requirements.in - echo "django-filter>=23.2,<24.0" >> requirements.in - echo "python-decouple>=3.8" >> requirements.in - echo "django-cors-headers>=4.6.0" >> requirements.in - echo "drf-yasg==1.21.10" >> requirements.in - pip-compile requirements.in - fi - pip install -r requirements.txt + run: pip install --upgrade pip && pip install -r requirements.txt - - name: Set Environment Variables + - name: Set up environment run: | echo "SECRET_KEY=${{ secrets.SECRET_KEY || 'test-secret-key' }}" >> $GITHUB_ENV - echo "DB_NAME=test_db" >> $GITHUB_ENV - echo "DB_USER=postgres" >> $GITHUB_ENV - echo "DB_PASSWORD=password" >> $GITHUB_ENV - echo "DB_HOST=localhost" >> $GITHUB_ENV - echo "DB_PORT=5432" >> $GITHUB_ENV + echo DB_NAME=test_db >> $GITHUB_ENV + echo DB_USER=postgres >> $GITHUB_ENV + echo DB_PASSWORD=password >> $GITHUB_ENV + echo DB_HOST=localhost >> $GITHUB_ENV + echo DB_PORT=5432 >> $GITHUB_ENV - name: Wait for PostgreSQL to be ready run: | - until pg_isready -h localhost -p 5432 -U postgres; do - echo "Waiting for PostgreSQL to be ready..." - sleep 5 - done + until pg_isready -h localhost -U postgres; do sleep 1; done - name: Run migrations run: python manage.py migrate diff --git a/api/utils.py b/api/utils.py index 1182a2a..c3faf1a 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,14 +1,33 @@ +import logging +import traceback +from typing import Optional from rest_framework.response import Response from rest_framework import status -def error_response(message, http_status=status.HTTP_400_BAD_REQUEST): +logger = logging.getLogger(__name__) + + +def error_response( + message: Optional[str] = None, + http_status: int = status.HTTP_400_BAD_REQUEST, + *, + exc: Optional[Exception] = None +): """ - Generates a response with the ‘error’ field and the specified HTTP status + Generates a generalized Response with the 'error' field. + If exc is passed, it logs the stack trace on the server """ - return Response({"error": message}, status=http_status) + if exc is not None: + # Logging of a full stack trade + logger.error("Internal error: %s\n%s", exc, traceback.format_exc()) + + # We return only a general message to the client + safe_message = message or "An internal server error occurred" + return Response({"error": safe_message}, status=http_status) + def status_response(message, http_status=None): """ Generates a response with the ‘status’ field and the specified HTTP status - """ - return Response({"status": message}, status=http_status) \ No newline at end of file + """ + return Response({"status": message}, status=http_status) diff --git a/projects/views.py b/projects/views.py index 124a75f..370deb9 100644 --- a/projects/views.py +++ b/projects/views.py @@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from rest_framework import viewsets, status -from rest_framework.exceptions import ValidationError from rest_framework.decorators import action, api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -133,7 +132,7 @@ def assign_role(self, request, pk=None): ProjectMembershipService.assign_role(project, target, role) except Exception as e: message = getattr(e, 'detail', str(e)) - return error_response(message) + return error_response(message, exc=e) return status_response("Role assigned") diff --git a/tasks/views.py b/tasks/views.py index 8de0eb1..10eae2e 100644 --- a/tasks/views.py +++ b/tasks/views.py @@ -112,10 +112,11 @@ def toggle_favorite(self, request, pk=None): "is_favorite": updated_task.is_favorite, }) except Exception as e: - logger.error(f"Error toggling favorite for task {pk}: {e}") + logger.exception(f"Error toggling favorite for task {pk}") return error_response( "Failed to update favorite status", status.HTTP_500_INTERNAL_SERVER_ERROR, + exc=e ) @action( @@ -143,10 +144,11 @@ def toggle_completed(self, request, project_pk=None, pk=None): } ) except Exception as e: - logger.error(f"Error toggling completion for task {pk}: {e}") + logger.exception(f"Error toggling completion for task {pk}") return error_response( "Failed to update completion status", - status=status.HTTP_500_INTERNAL_SERVER_ERROR, + status.HTTP_500_INTERNAL_SERVER_ERROR, + exc=e ) @action( @@ -186,11 +188,13 @@ def move_task(self, request, project_pk=None, pk=None): TaskService.move_task_to_project(task, project_id, request.user) return status_response("Task moved successfully") except ValueError as e: - return error_response(str(e), status.HTTP_404_NOT_FOUND) + return error_response(str(e), status.HTTP_404_NOT_FOUND, exc=e) except Exception as e: - logger.error(f"Error moving task: {e}", exc_info=True) + logger.exception(f"Error moving task: {e}") return error_response( - "Failed to move task", status.HTTP_500_INTERNAL_SERVER_ERROR + "Failed to move task", + status.HTTP_500_INTERNAL_SERVER_ERROR, + exc=e, ) @action( diff --git a/users/views.py b/users/views.py index d3f2822..0ded8ec 100644 --- a/users/views.py +++ b/users/views.py @@ -21,7 +21,7 @@ class AuthViewSet(viewsets.GenericViewSet): def get_serializer_class(self): if getattr(self, 'swagger_fake_view', False): return serializers.Serializer - + if self.action == 'register': return UserRegistrationSerializer elif self.action == 'login': @@ -51,13 +51,14 @@ def logout(self, request): data = UserService.logout_user(refresh_token) return Response(data, status=status.HTTP_200_OK) except ValueError as e: - return error_response(str(e)) + return error_response(str(e), exc=e) except Exception as e: return error_response( - f"Unexpected error {str(e)}", + "An unexpected error occured", status.HTTP_500_INTERNAL_SERVER_ERROR, + exc=e, ) - + class UserViewSet(viewsets.GenericViewSet): """ @@ -74,4 +75,4 @@ def profile(self, request): @action(detail=False, methods=['put', 'patch']) def update_profile(self, request): data = UserService.update_profile(request.user, request.data) - return Response(data) \ No newline at end of file + return Response(data)