From d8bb00ec1e0d3411feb0a01e77ac2be18aa818c7 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Wed, 3 Jul 2024 12:16:39 +0300 Subject: [PATCH 01/15] feat(game-history): Initial setup and configuration of game-history service - Created Dockerfile for building the game-history service container - Added tools.sh script to initialize and configure PostgreSQL, apply Django migrations, and start the Django application - Implemented init_database.sh script to initialize the PostgreSQL database, create necessary users, and configure database settings - Configured supervisord to manage the Django application and database initialization - Added run_consumer.sh script to wait for Django server availability and start the consumer - Updated supervisord.conf to run the tools.sh script for initializing the service - Ensured proper permissions and ownership for necessary directories and files in the Docker container - Verified PostgreSQL database connection and applied Django migrations - Successfully ran pytest to verify initial tests for the game-history service --- Backend/auth_service/requirements.txt | 3 +- Backend/game_history/Dockerfile | 57 ++++++ Backend/game_history/game_data/__init__.py | 0 Backend/game_history/game_data/admin.py | 3 + Backend/game_history/game_data/apps.py | 6 + .../game_data/migrations/__init__.py | 0 Backend/game_history/game_data/models.py | 17 ++ .../game_history/game_data/rabbitmq_utils.py | 44 +++++ Backend/game_history/game_data/serializers.py | 9 + Backend/game_history/game_data/tests.py | 3 + Backend/game_history/game_data/urls.py | 26 +++ Backend/game_history/game_data/views.py | 89 +++++++++ Backend/game_history/game_history/__init__.py | 0 Backend/game_history/game_history/asgi.py | 16 ++ Backend/game_history/game_history/consumer.py | 16 ++ Backend/game_history/game_history/manage.py | 22 +++ Backend/game_history/game_history/settings.py | 170 ++++++++++++++++++ .../game_history/tests/conftest.py | 103 +++++++++++ .../game_history/tests/test_game_history.py | 47 +++++ Backend/game_history/game_history/urls.py | 12 ++ Backend/game_history/game_history/wsgi.py | 16 ++ Backend/game_history/init_database.sh | 42 +++++ Backend/game_history/requirements.txt | 34 ++++ Backend/game_history/run_consumer.sh | 17 ++ Backend/game_history/supervisord.conf | 29 +++ Backend/game_history/tools.sh | 27 +++ Backend/user_management/requirements.txt | 5 +- Backend/user_management/tools.sh | 2 +- .../user_management/users/rabbitmq_utils.py | 3 +- Frontend/nginx.conf | 11 ++ Makefile | 3 +- docker-compose.yml | 23 ++- pytest.ini | 4 + sample env | 12 -- 34 files changed, 848 insertions(+), 23 deletions(-) create mode 100644 Backend/game_history/Dockerfile create mode 100644 Backend/game_history/game_data/__init__.py create mode 100644 Backend/game_history/game_data/admin.py create mode 100644 Backend/game_history/game_data/apps.py create mode 100644 Backend/game_history/game_data/migrations/__init__.py create mode 100644 Backend/game_history/game_data/models.py create mode 100644 Backend/game_history/game_data/rabbitmq_utils.py create mode 100644 Backend/game_history/game_data/serializers.py create mode 100644 Backend/game_history/game_data/tests.py create mode 100644 Backend/game_history/game_data/urls.py create mode 100644 Backend/game_history/game_data/views.py create mode 100644 Backend/game_history/game_history/__init__.py create mode 100644 Backend/game_history/game_history/asgi.py create mode 100644 Backend/game_history/game_history/consumer.py create mode 100755 Backend/game_history/game_history/manage.py create mode 100644 Backend/game_history/game_history/settings.py create mode 100644 Backend/game_history/game_history/tests/conftest.py create mode 100644 Backend/game_history/game_history/tests/test_game_history.py create mode 100644 Backend/game_history/game_history/urls.py create mode 100644 Backend/game_history/game_history/wsgi.py create mode 100644 Backend/game_history/init_database.sh create mode 100644 Backend/game_history/requirements.txt create mode 100644 Backend/game_history/run_consumer.sh create mode 100644 Backend/game_history/supervisord.conf create mode 100644 Backend/game_history/tools.sh create mode 100644 pytest.ini delete mode 100644 sample env diff --git a/Backend/auth_service/requirements.txt b/Backend/auth_service/requirements.txt index d15d9e4..c296b7f 100644 --- a/Backend/auth_service/requirements.txt +++ b/Backend/auth_service/requirements.txt @@ -16,5 +16,6 @@ pytest==8.2.1; python_version >= '3.8' sqlparse==0.5.0; python_version >= '3.8' tomli==2.0.1; python_version <= '3.12' typing-extensions==4.12.1; python_version <= '3.12' +# gunicorn==20.1.0; python_version <= '3.12' django-environ -daphne \ No newline at end of file +daphne diff --git a/Backend/game_history/Dockerfile b/Backend/game_history/Dockerfile new file mode 100644 index 0000000..a798e14 --- /dev/null +++ b/Backend/game_history/Dockerfile @@ -0,0 +1,57 @@ +FROM alpine:3.20 + +ENV PYTHONUNBUFFERED=1 +ENV LANG=C.UTF-8 +ENV PYTHONPATH=/app:/app/game_data:/app/game_history + +# Update and install dependencies +# trunk-ignore(hadolint/DL3018) +RUN apk update && apk add --no-cache python3 py3-pip \ + postgresql16 postgresql16-client \ + bash supervisor curl openssl bash \ + build-base libffi-dev python3-dev + + +# Set work directory +RUN mkdir /run/postgresql && \ + chown postgres:postgres /run/postgresql && \ + mkdir /app && chown -R postgres:postgres /app + +WORKDIR /app/ + +# Install Python virtual environment +RUN python3 -m venv venv && chown -R postgres:postgres venv + +# Copy application code and adjust permissions +COPY --chown=postgres:postgres ./Backend/game_history/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY --chown=postgres:postgres ./Backend/game_history/requirements.txt . +COPY --chown=postgres:postgres ./Backend/game_history/game_history /app/game_history +COPY --chown=postgres:postgres ./Backend/game_history/game_data /app/game_data +COPY --chown=postgres:postgres ./Backend/game_history/tools.sh /app +COPY --chown=postgres:postgres ./Backend/game_history/init_database.sh /app +COPY --chown=postgres:postgres ./Backend/game_history/run_consumer.sh /app +RUN chmod +x /app/tools.sh /app/init_database.sh /app/run_consumer.sh + +# Install Python packages +RUN . venv/bin/activate && pip install -r requirements.txt + +# Create log files and set permissions +RUN touch /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err && \ + chown -R postgres:postgres /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err + +# Ensure supervisord and scripts are executable and owned by postgres +RUN chown -R postgres:postgres /etc/supervisor && \ + chown -R postgres:postgres /usr/bin/supervisord && \ + chown -R postgres:postgres /etc/supervisor/conf.d/supervisord.conf && \ + chown -R postgres:postgres /app && \ + chown -R postgres:postgres /var/log && \ + chown -R postgres:postgres /app/venv && \ + chown -R postgres:postgres /app/game_history && \ + chown -R postgres:postgres /app/game_data && \ + chmod +x /usr/bin/supervisord + +USER postgres + +HEALTHCHECK --interval=30s --timeout=2s --start-period=5s --retries=3 CMD curl -sSf http://localhost:8002/game-history/ > /dev/null && echo "success" || echo "failure" + +ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/Backend/game_history/game_data/__init__.py b/Backend/game_history/game_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_history/game_data/admin.py b/Backend/game_history/game_data/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Backend/game_history/game_data/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Backend/game_history/game_data/apps.py b/Backend/game_history/game_data/apps.py new file mode 100644 index 0000000..bbe8172 --- /dev/null +++ b/Backend/game_history/game_data/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GameDataConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'game_data' diff --git a/Backend/game_history/game_data/migrations/__init__.py b/Backend/game_history/game_data/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_history/game_data/models.py b/Backend/game_history/game_data/models.py new file mode 100644 index 0000000..fbe9448 --- /dev/null +++ b/Backend/game_history/game_data/models.py @@ -0,0 +1,17 @@ +# game_data/models.py + +from django.db import models + +class GameHistory(models.Model): + GameID = models.AutoField(primary_key=True) + player1ID = models.IntegerField(unique=True) + player2ID = models.IntegerField(unique=True) + winner = models.CharField(max_length=255) + score = models.CharField(max_length=50) + match_date = models.DateTimeField(auto_now_add=True) + + class Meta: + app_label = 'game_data' + + def __str__(self): + return f"{self.player1} vs {self.player2} - Winner: {self.winner} - Score: {self.score}" diff --git a/Backend/game_history/game_data/rabbitmq_utils.py b/Backend/game_history/game_data/rabbitmq_utils.py new file mode 100644 index 0000000..d9bdbb2 --- /dev/null +++ b/Backend/game_history/game_data/rabbitmq_utils.py @@ -0,0 +1,44 @@ +import pika # RabbitMQ library +import json +from django.conf import settings + +class RabbitMQManager: + _connection = None # Connection to RabbitMQ server is none by default so that we can check if it is connected or not + + @classmethod # Class method to connect to RabbitMQ server + def get_connection(cls): + if not cls._connection or cls._connection.is_closed: # If connection is not established or connection is closed + cls._connection = cls.create_connection() + return cls._connection # Return the connection to RabbitMQ server + + @classmethod # Class method to create connection to RabbitMQ server + def create_connection(cls): + credentials = pika.PlainCredentials(settings.RABBITMQ_USER, settings.RABBITMQ_PASS) # Create credentials to connect to RabbitMQ server + parameters = pika.ConnectionParameters(settings.RABBITMQ_HOST, settings.RABBITMQ_PORT, "/", credentials=credentials) # Create connection parameters to RabbitMQ server + return pika.BlockingConnection(parameters) # Create a blocking connection to RabbitMQ server because we want to wait for the connection to be established before proceeding + + @staticmethod # Static method to publish message to RabbitMQ server + def publish_message(queue_name, message): + connection = RabbitMQManager.get_connection() + channel = connection.channel() # Create a channel to RabbitMQ server to publish message + channel.queue_declare(queue=queue_name, durable=True) # Declare a queue to RabbitMQ server with the given name and make it durable so that it persists even if RabbitMQ server restarts + channel.basic_publish( + exchange='', # Publish message to default exchange + routing_key=queue_name, # Publish message to the given queue + body=json.dumps(message), # Convert message to JSON format before publishing + properties=pika.BasicProperties(delivery_mode=2) # Make the message persistent so that it is not lost even if RabbitMQ server restarts + ) + connection.close() + + @staticmethod # Static method to consume message from RabbitMQ server + def consume_message(queue_name, callback): # Callback function to be called when a message is received + connection = RabbitMQManager.get_connection() + channel = connection.channel() # Create a channel to RabbitMQ server to consume message + channel.queue_declare(queue=queue_name, durable=True) # Declare a queue to RabbitMQ server with the given name and make it durable so that it persists even if RabbitMQ server restarts + + def wrapper(ch, method, properties, body): # Wrapper function to call the callback function with the received message + callback(json.loads(body)) # Convert the received message from JSON format before calling the callback function + ch.basic_ack(delivery_tag=method.delivery_tag) # Acknowledge the message so that it is removed from the queue + + channel.basic_consume(queue=queue_name, on_message_callback=wrapper) # Consume message from the given queue and call the wrapper function when a message is received + channel.start_consuming() # Start consuming messages from RabbitMQ server diff --git a/Backend/game_history/game_data/serializers.py b/Backend/game_history/game_data/serializers.py new file mode 100644 index 0000000..742ab0a --- /dev/null +++ b/Backend/game_history/game_data/serializers.py @@ -0,0 +1,9 @@ +# game_data/serializers.py + +from rest_framework import serializers +from .models import GameHistory + +class GameHistorySerializer(serializers.ModelSerializer): + class Meta: + model = GameHistory + fields = '__all__' diff --git a/Backend/game_history/game_data/tests.py b/Backend/game_history/game_data/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Backend/game_history/game_data/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Backend/game_history/game_data/urls.py b/Backend/game_history/game_data/urls.py new file mode 100644 index 0000000..cf13a12 --- /dev/null +++ b/Backend/game_history/game_data/urls.py @@ -0,0 +1,26 @@ +from django.urls import path + +from .views import GameHistoryViewSet + +urlpatterns = [ + path( + "game-history/", + GameHistoryViewSet.as_view( + { + "get": "game_history_list", + } + ), + name="game-history-list", + ), + path( + "game-history//", + GameHistoryViewSet.as_view( + { + "get": "retrieve_game_history", + "put": "update_game_history", + "delete": "destroy_game_history", + } + ), + name="game-history-detail", + ), +] diff --git a/Backend/game_history/game_data/views.py b/Backend/game_history/game_data/views.py new file mode 100644 index 0000000..8807894 --- /dev/null +++ b/Backend/game_history/game_data/views.py @@ -0,0 +1,89 @@ +# game_data/views.py + +from rest_framework import viewsets +from .models import GameHistory +from .serializers import GameHistorySerializer +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from rest_framework import status +class GameHistoryViewSet(viewsets.ModelViewSet): + queryset = GameHistory.objects.all() + serializer_class = GameHistorySerializer + + def create_game_history(self, request): + data = request.data + serializer = GameHistorySerializer(data=data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def game_history_list(self, request): + """ + Method to get the list of game history. + + This method gets the list of game history from the database and returns the list of game history. + + Args: + request: The request object. + + Returns: + Response: The response object containing the list of game history. + """ + game_history = GameHistory.objects.all() + serializer = GameHistorySerializer(game_history, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve_game_history(self, request, pk=None): + """ + Method to retrieve a game history. + + This method retrieves a game history from the database using the game history id and returns the game history data. + + Args: + request: The request object. + pk: The primary key of the game history. + + Returns: + Response: The response object containing the game history data. + """ + data = get_object_or_404(GameHistory, id=pk) + serializer = GameHistorySerializer(data) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update_game_history(self, request, pk=None): + """ + Method to update a game history. + + This method updates a game history in the database using the game history id and the data in the request. + + Args: + request: The request object containing the game history data. + pk: The primary key of the game history. + + Returns: + Response: The response object containing the updated game history data. + """ + data = get_object_or_404(GameHistory, id=pk) + serializer = GameHistorySerializer(data, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy_game_history(self, request, pk=None): + """ + Method to delete a game history. + + This method deletes a game history from the database using the game history id. + + Args: + request: The request object. + pk: The primary key of the game history. + + Returns: + Response: The response object containing the status of the deletion. + """ + data = get_object_or_404(GameHistory, id=pk) + data.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_history/game_history/__init__.py b/Backend/game_history/game_history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_history/game_history/asgi.py b/Backend/game_history/game_history/asgi.py new file mode 100644 index 0000000..0465109 --- /dev/null +++ b/Backend/game_history/game_history/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for game_history project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + +application = get_asgi_application() diff --git a/Backend/game_history/game_history/consumer.py b/Backend/game_history/game_history/consumer.py new file mode 100644 index 0000000..5664e08 --- /dev/null +++ b/Backend/game_history/game_history/consumer.py @@ -0,0 +1,16 @@ +import os + +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "game_history.settings") +django.setup() + + +def start_consumer(): + from users.views import UserViewSet + + UserViewSet().start_consumer() + + +if __name__ == "__main__": + start_consumer() diff --git a/Backend/game_history/game_history/manage.py b/Backend/game_history/game_history/manage.py new file mode 100755 index 0000000..a05f77a --- /dev/null +++ b/Backend/game_history/game_history/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Backend/game_history/game_history/settings.py b/Backend/game_history/game_history/settings.py new file mode 100644 index 0000000..f4b3906 --- /dev/null +++ b/Backend/game_history/game_history/settings.py @@ -0,0 +1,170 @@ +""" +Django settings for game_history project. + +Generated by 'django-admin startproject' using Django 5.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" +import os +from datetime import timedelta +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + '[::1]', + 'game-history', + 'game-history:8002', +] + +RABBITMQ_HOST = os.getenv("RABBITMQ_HOST") +RABBITMQ_USER = os.getenv("RABBITMQ_USER") +RABBITMQ_PASS = os.getenv("RABBITMQ_PASS") +RABBITMQ_PORT = os.getenv("RABBITMQ_PORT") + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'game_history', + 'game_data', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', +] + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": False, # If True, refresh tokens will rotate, meaning that a new token is returned with each request to the refresh endpoint. for example, if a user is logged in on multiple devices, rotating refresh tokens will cause all devices to be logged out when the user logs out on one device. + "BLACKLIST_AFTER_ROTATION": True, # If True, the refresh token will be blacklisted after it is used to obtain a new access token. This means that if a refresh token is stolen, it can only be used once to obtain a new access token. This is useful if rotating refresh tokens is enabled, but can cause problems if a refresh token is shared between multiple clients. + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), +} + +# Add REST framework settings for JWT authentication +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + ], + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.JSONParser', + ], +} + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'game_history.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +ASGI_APPLICATION = 'game_history.asgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'root', + 'PASSWORD': 'root', + 'HOST': 'localhost', + 'PORT': '5432', + } +} + + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +CORS_ORIGIN_ALLOW_ALL = True + diff --git a/Backend/game_history/game_history/tests/conftest.py b/Backend/game_history/game_history/tests/conftest.py new file mode 100644 index 0000000..d9efd09 --- /dev/null +++ b/Backend/game_history/game_history/tests/conftest.py @@ -0,0 +1,103 @@ +import os +import django +from django.conf import settings +import pytest +from datetime import timedelta + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + +if not settings.configured: + settings.configure( + DEBUG=True, + DATABASES={ + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "postgres", + "PORT": "5432", + "HOST": "localhost", + "ATOMIC_REQUESTS": True, + } + }, + INSTALLED_APPS=[ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'game_history', + 'game_data', + ], + MIDDLEWARE=[ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ], + ROOT_URLCONF='game_history.urls', + TEMPLATES=[ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, + ], + WSGI_APPLICATION='game_history.wsgi.application', + SECRET_KEY='django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%', + ALLOWED_HOSTS=['localhost', '127.0.0.1', '[::1]', 'game-history', 'game-history:8002'], + RABBITMQ_HOST='localhost', + RABBITMQ_USER='user', + RABBITMQ_PASS='pass', + RABBITMQ_PORT='5672', + REST_FRAMEWORK={ + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + }, + SIMPLE_JWT={ + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_OBTAIN_SERIALIZER': 'user_auth.serializers.CustomTokenObtainPairSerializer', + }, + LANGUAGE_CODE='en-us', + TIME_ZONE='UTC', + USE_I18N=True, + USE_L10N=True, + USE_TZ=True, + STATIC_URL='/static/', + DEFAULT_AUTO_FIELD='django.db.models.BigAutoField', + CORS_ORIGIN_ALLOW_ALL=True, + ) + +django.setup() + +@pytest.fixture(scope='session', autouse=True) +def django_db_setup(): + settings.DATABASES['default'] = { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'localhost', + 'PORT': '5432', + 'ATOMIC_REQUESTS': True, + } diff --git a/Backend/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/tests/test_game_history.py new file mode 100644 index 0000000..def7e9a --- /dev/null +++ b/Backend/game_history/game_history/tests/test_game_history.py @@ -0,0 +1,47 @@ +# game_history/tests/test_game_history.py + +import pytest +from django.urls import reverse +from rest_framework.test import APIClient +from rest_framework import status +from game_data.models import GameHistory + +@pytest.fixture +def api_client(): + return APIClient() + +@pytest.mark.django_db +def test_create_game_history(api_client): + url = reverse('gamehistory-list') # reverse() is used to generate the URL for the view name 'gamehistory-list' + data = { + 'player1': 'player1', + 'player2': 'player2', + 'winner': 'player1', + 'score': '21-15' + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert GameHistory.objects.count() == 1 + game_history = GameHistory.objects.first() + assert game_history.player1 == 'player1' + assert game_history.player2 == 'player2' + assert game_history.winner == 'player1' + assert game_history.score == '21-15' + +@pytest.mark.django_db +def test_list_game_histories(api_client): + game1 = GameHistory.objects.create(player1='player1', player2='player2', winner='player1', score='21-15') + game2 = GameHistory.objects.create(player1='player3', player2='player4', winner='player4', score='19-21') + + url = reverse('gamehistory-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 + assert response.data[0]['player1'] == game1.player1 + assert response.data[0]['player2'] == game1.player2 + assert response.data[0]['winner'] == game1.winner + assert response.data[0]['score'] == game1.score + assert response.data[1]['player1'] == game2.player1 + assert response.data[1]['player2'] == game2.player2 + assert response.data[1]['winner'] == game2.winner + assert response.data[1]['score'] == game2.score diff --git a/Backend/game_history/game_history/urls.py b/Backend/game_history/game_history/urls.py new file mode 100644 index 0000000..6dc3bdb --- /dev/null +++ b/Backend/game_history/game_history/urls.py @@ -0,0 +1,12 @@ +# game_history/urls.py + +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from game_data.views import GameHistoryViewSet + +router = DefaultRouter() +router.register(r'game-history', GameHistoryViewSet, basename='gamehistory') + +urlpatterns = [ + path('', include(router.urls)), +] diff --git a/Backend/game_history/game_history/wsgi.py b/Backend/game_history/game_history/wsgi.py new file mode 100644 index 0000000..b238610 --- /dev/null +++ b/Backend/game_history/game_history/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for game_history project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') + +application = get_wsgi_application() diff --git a/Backend/game_history/init_database.sh b/Backend/game_history/init_database.sh new file mode 100644 index 0000000..5a87858 --- /dev/null +++ b/Backend/game_history/init_database.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Create necessary directories with appropriate permissions +cd / +mkdir -p /run/postgresql +chown postgres:postgres /run/postgresql/ + +# Initialize the database +initdb -D /var/lib/postgresql/data + +# Switch to the postgres user and run the following commands +mkdir -p /var/lib/postgresql/data +initdb -D /var/lib/postgresql/data + +# Append configurations to pg_hba.conf and postgresql.conf as the postgres user +echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf +echo "listen_addresses='*'" >> /var/lib/postgresql/data/postgresql.conf + +# Remove the unix_socket_directories line from postgresql.conf as the postgres user +sed -i "/^unix_socket_directories = /d" /var/lib/postgresql/data/postgresql.conf + +# Ensure the postgres user owns the data directory +chown -R postgres:postgres /var/lib/postgresql/data + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +exec postgres -D /var/lib/postgresql/data & + +# Wait for PostgreSQL to start (you may need to adjust the sleep time) +sleep 5 + +# Create a new PostgreSQL user and set the password +psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" +psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" + +# Stop the PostgreSQL server after setting the password +pg_ctl stop -D /var/lib/postgresql/data + +sleep 5 + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +pg_ctl start -D /var/lib/postgresql/data diff --git a/Backend/game_history/requirements.txt b/Backend/game_history/requirements.txt new file mode 100644 index 0000000..9cd5e52 --- /dev/null +++ b/Backend/game_history/requirements.txt @@ -0,0 +1,34 @@ +# # Backend/game_history/requirements.txt + +# Django>4.2.7 +# djangorestframework>=3.15.2 +# djangorestframework-simplejwt>=4.9.0 +# psycopg2-binary>=2.8.6 +# pika>=1.1.0 +# pytest>=6.2.4 +# pytest-django>=4.4.0 +# django-cors-headers>=4.4.0 +# rest_framework + +-i https://pypi.org/simple +asgiref==3.8.1; python_version >= '3.8' +django==5.0.6; python_version >= '3.10' +django-cors-headers==4.3.1; python_version >= '3.8' +django-mysql==4.13.0; python_version >= '3.8' +djangorestframework==3.15.1; python_version >= '3.6' +djangorestframework-simplejwt; python_version >= '3.6' +pgsql; python_version >= '3.7' +djangorestframework-simplejwt[crypto]; python_version >= '3.6' +exceptiongroup==1.2.1; python_version <= '3.12' +iniconfig==2.0.0; python_version >= '3.7' +packaging==24.0; python_version >= '3.7' +pika==1.3.2; python_version >= '3.7' +pluggy==1.5.0; python_version >= '3.8' +psycopg2-binary==2.9.9; python_version >= '3.7' +pytest==8.2.1; python_version >= '3.8' +sqlparse==0.5.0; python_version >= '3.8' +tomli==2.0.1; python_version <= '3.12' +typing-extensions==4.12.1; python_version <= '3.12' +# gunicorn==20.1.0; python_version <= '3.12' +django-environ +daphne diff --git a/Backend/game_history/run_consumer.sh b/Backend/game_history/run_consumer.sh new file mode 100644 index 0000000..364455c --- /dev/null +++ b/Backend/game_history/run_consumer.sh @@ -0,0 +1,17 @@ +# Backend/game_history/run_consumer.sh +#!/bin/bash + +# URL of the Django application +DJANGO_URL="http://localhost:8002/some-endpoint/" # Adjust the endpoint as necessary + +# Wait until Django server is available +while ! curl -s "${DJANGO_URL}" >/dev/null; do + echo "Waiting for Django server at ${DJANGO_URL}..." + sleep 5 +done + +# Activate the virtual environment and start the consumer +source venv/bin/activate +python /app/game_history/consumer.py +echo "Django server is up at ${DJANGO_URL}" +exec "$@" diff --git a/Backend/game_history/supervisord.conf b/Backend/game_history/supervisord.conf new file mode 100644 index 0000000..ed777af --- /dev/null +++ b/Backend/game_history/supervisord.conf @@ -0,0 +1,29 @@ +[supervisord] +nodaemon=true + +# [program:django] +# command=/app/venv/bin/python /app/manage.py runserver 0.0.0.0:8002 +# user=postgres +# autostart=true +# autorestart=true +# stdout_logfile=/var/log/django.log +# stderr_logfile=/var/log/django.err + +[program:django] +command=/app/venv/bin/python /app/game_history/manage.py runserver 0.0.0.0:8000 +user=postgres +autostart=true +autorestart=true +stderr_logfile=/var/log/django.err.log +stdout_logfile=/var/log/django.out.log +user=postgres + + +[program:init_database] +command=/bin/bash /app/init_database.sh +user=postgres +autostart=true +autorestart=true +startsecs=0 +stdout_logfile=/var/log/init_database.log +stderr_logfile=/var/log/init_database.err diff --git a/Backend/game_history/tools.sh b/Backend/game_history/tools.sh new file mode 100644 index 0000000..65bf2f7 --- /dev/null +++ b/Backend/game_history/tools.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Run the initialization script +sh /app/init_database.sh + +# Activate the virtual environment and install dependencies +source venv/bin/activate +pip install -r requirements.txt +pip install tzdata + +# Wait for PostgreSQL to be available +while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do + echo >&2 "Postgres is unavailable - sleeping" + sleep 5 +done + +export DJANGO_SETTINGS_MODULE=game_history.settings + +# Apply Django migrations + +python3 /app/game_history/manage.py makemigrations game_data +python3 /app/game_history/manage.py makemigrations game_history +python3 /app/game_history/manage.py migrate + +# Start the Django application +cd /app/game_history +daphne -b 0.0.0.0 -p 8002 game_history.asgi:application diff --git a/Backend/user_management/requirements.txt b/Backend/user_management/requirements.txt index 0f16fac..5bda2cf 100644 --- a/Backend/user_management/requirements.txt +++ b/Backend/user_management/requirements.txt @@ -17,6 +17,7 @@ pytest==8.2.1; python_version >= '3.8' sqlparse==0.5.0; python_version >= '3.8' tomli==2.0.1; python_version <= '3.12' typing-extensions==4.12.1; python_version <= '3.12' -gunicorn==20.1.0; python_version <= '3.12' +# gunicorn==20.1.0; python_version <= '3.12' django-environ -daphne \ No newline at end of file +daphne +postgresql diff --git a/Backend/user_management/tools.sh b/Backend/user_management/tools.sh index 1cde498..c8ec34c 100644 --- a/Backend/user_management/tools.sh +++ b/Backend/user_management/tools.sh @@ -1,7 +1,7 @@ #! /bin/bash sh /app/init_database.sh -# trunk-ignore(shellcheck/SC1091) + source venv/bin/activate pip install -r requirements.txt pip install tzdata diff --git a/Backend/user_management/user_management/users/rabbitmq_utils.py b/Backend/user_management/user_management/users/rabbitmq_utils.py index 68ba21d..56b8c24 100644 --- a/Backend/user_management/user_management/users/rabbitmq_utils.py +++ b/Backend/user_management/user_management/users/rabbitmq_utils.py @@ -16,8 +16,7 @@ def get_connection(cls): def _create_connection(cls): credentials = pika.PlainCredentials(settings.RABBITMQ_USER, settings.RABBITMQ_PASS) parameters = pika.ConnectionParameters( - settings.RABBITMQ_HOST, settings.RABBITMQ_PORT, "/", credentials - ) + settings.RABBITMQ_HOST, settings.RABBITMQ_PORT, "/", credentials=credentials) return pika.BlockingConnection(parameters) diff --git a/Frontend/nginx.conf b/Frontend/nginx.conf index 332f9d6..4fa595c 100644 --- a/Frontend/nginx.conf +++ b/Frontend/nginx.conf @@ -24,6 +24,9 @@ http { server user-service:8001; } + upstream game-history { + server game-history:8002; + } server { listen 80; server_name localhost; @@ -65,5 +68,13 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + + location /game-history/ { + proxy_pass http://game-history; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } } } diff --git a/Makefile b/Makefile index 695d433..8e7e420 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,8 @@ clean: docker volume prune -f docker network prune -f -# Free up the port if it's already allocated + +# # Free up the port if it's already allocated # .PHONY: free-port # free-port: # @echo "Checking for allocated port 15672..." diff --git a/docker-compose.yml b/docker-compose.yml index d9c1e17..0652306 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,8 +26,8 @@ services: dockerfile: Backend/user_management/Dockerfile env_file: - .env - # ports: - # - 8001:8001 + ports: + - 8001:8001 networks: - transcendence_network depends_on: @@ -41,8 +41,23 @@ services: dockerfile: Backend/auth_service/Dockerfile env_file: - .env - # ports: - # - 8000:8000 + ports: + - 8000:8000 + networks: + - transcendence_network + depends_on: + - rabbitmq + + game-history: + container_name: game-history + image: game-history + build: + context: . + dockerfile: Backend/game_history/Dockerfile + env_file: + - .env + ports: + - 8002:8002 networks: - transcendence_network depends_on: diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3c576a6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +# pytest.ini +[pytest] +DJANGO_SETTINGS_MODULE = game_history.settings +python_files = test_game_history.py diff --git a/sample env b/sample env deleted file mode 100644 index cc44359..0000000 --- a/sample env +++ /dev/null @@ -1,12 +0,0 @@ -DB_USER = "root" -DB_PASS = "root" - -RABBITMQ_DEFAULT_USER="user" -RABBITMQ_DEFAULT_PASS="pass" - -RABBITMQ_HOST="rabbitmq" -RABBITMQ_USER="user" -RABBITMQ_PASS="pass" -RABBITMQ_PORT="5672" - -PGPASSWORD='root' \ No newline at end of file From 30bbe66cd54aebb328331bb946a62c61d64c2871 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Wed, 3 Jul 2024 13:12:47 +0300 Subject: [PATCH 02/15] Issue #15: Set Up Django Microservice for Game History - Initialized Django project and game history app. - Configured Django settings and connected to PostgreSQL database. - Defined GameHistory model and created migrations. - Implemented CRUD API endpoints for game history. - Updated test cases to match the new structure and endpoints. - Wrote tests for creating and listing game history records. - Documentation is not ready yet --- Backend/game_history/game_data/models.py | 20 +- Backend/game_history/game_data/urls.py | 10 +- Backend/game_history/game_data/views.py | 186 ++++++++++++------ .../game_history/tests/test_game_history.py | 93 +++++++-- Backend/game_history/game_history/urls.py | 9 +- Makefile | 15 -- 6 files changed, 210 insertions(+), 123 deletions(-) diff --git a/Backend/game_history/game_data/models.py b/Backend/game_history/game_data/models.py index fbe9448..3f71eb7 100644 --- a/Backend/game_history/game_data/models.py +++ b/Backend/game_history/game_data/models.py @@ -1,17 +1,13 @@ -# game_data/models.py - +# models.py from django.db import models class GameHistory(models.Model): - GameID = models.AutoField(primary_key=True) - player1ID = models.IntegerField(unique=True) - player2ID = models.IntegerField(unique=True) - winner = models.CharField(max_length=255) - score = models.CharField(max_length=50) - match_date = models.DateTimeField(auto_now_add=True) - - class Meta: - app_label = 'game_data' + game_id = models.AutoField(primary_key=True) + player1_id = models.IntegerField() # Reference to UserID in UserDB + player2_id = models.IntegerField() # Reference to UserID in UserDB + winner_id = models.IntegerField() # Reference to UserID in UserDB + start_time = models.DateTimeField() + end_time = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.player1} vs {self.player2} - Winner: {self.winner} - Score: {self.score}" + return f"Game {self.game_id}: {self.player1_id} vs {self.player2_id} - Winner: {self.winner_id}" diff --git a/Backend/game_history/game_data/urls.py b/Backend/game_history/game_data/urls.py index cf13a12..396cdbc 100644 --- a/Backend/game_history/game_data/urls.py +++ b/Backend/game_history/game_data/urls.py @@ -1,5 +1,4 @@ from django.urls import path - from .views import GameHistoryViewSet urlpatterns = [ @@ -7,7 +6,8 @@ "game-history/", GameHistoryViewSet.as_view( { - "get": "game_history_list", + "get": "list", + "post": "create" } ), name="game-history-list", @@ -16,9 +16,9 @@ "game-history//", GameHistoryViewSet.as_view( { - "get": "retrieve_game_history", - "put": "update_game_history", - "delete": "destroy_game_history", + "get": "retrieve", + "put": "update", + "delete": "destroy", } ), name="game-history-detail", diff --git a/Backend/game_history/game_data/views.py b/Backend/game_history/game_data/views.py index 8807894..317a0e1 100644 --- a/Backend/game_history/game_data/views.py +++ b/Backend/game_history/game_data/views.py @@ -1,89 +1,149 @@ -# game_data/views.py - -from rest_framework import viewsets +# # game_data/views.py + +# from rest_framework import viewsets +# from .models import GameHistory +# from .serializers import GameHistorySerializer +# from rest_framework.response import Response +# from django.shortcuts import get_object_or_404 +# from rest_framework import status +# class GameHistoryViewSet(viewsets.ModelViewSet): +# queryset = GameHistory.objects.all() +# serializer_class = GameHistorySerializer + +# def create_game_history(self, request): +# data = request.data +# serializer = GameHistorySerializer(data=data) +# if serializer.is_valid(): +# serializer.save() +# return Response(serializer.data, status=status.HTTP_201_CREATED) +# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# def game_history_list(self, request): +# """ +# Method to get the list of game history. + +# This method gets the list of game history from the database and returns the list of game history. + +# Args: +# request: The request object. + +# Returns: +# Response: The response object containing the list of game history. +# """ +# game_history = GameHistory.objects.all() +# serializer = GameHistorySerializer(game_history, many=True) +# return Response(serializer.data, status=status.HTTP_200_OK) + +# def retrieve_game_history(self, request, pk=None): +# """ +# Method to retrieve a game history. + +# This method retrieves a game history from the database using the game history id and returns the game history data. + +# Args: +# request: The request object. +# pk: The primary key of the game history. + +# Returns: +# Response: The response object containing the game history data. +# """ +# data = get_object_or_404(GameHistory, id=pk) +# serializer = GameHistorySerializer(data) +# return Response(serializer.data, status=status.HTTP_200_OK) + +# def update_game_history(self, request, pk=None): +# """ +# Method to update a game history. + +# This method updates a game history in the database using the game history id and the data in the request. + +# Args: +# request: The request object containing the game history data. +# pk: The primary key of the game history. + +# Returns: +# Response: The response object containing the updated game history data. +# """ +# data = get_object_or_404(GameHistory, id=pk) +# serializer = GameHistorySerializer(data, data=request.data) +# if serializer.is_valid(): +# serializer.save() +# return Response(serializer.data, status=status.HTTP_201_CREATED) +# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# def destroy_game_history(self, request, pk=None): +# """ +# Method to delete a game history. + +# This method deletes a game history from the database using the game history id. + +# Args: +# request: The request object. +# pk: The primary key of the game history. + +# Returns: +# Response: The response object containing the status of the deletion. +# """ +# data = get_object_or_404(GameHistory, id=pk) +# data.delete() +# return Response(status=status.HTTP_204_NO_CONTENT) + +from rest_framework import viewsets, status from .models import GameHistory from .serializers import GameHistorySerializer from rest_framework.response import Response from django.shortcuts import get_object_or_404 -from rest_framework import status + class GameHistoryViewSet(viewsets.ModelViewSet): - queryset = GameHistory.objects.all() + """ + A viewset for viewing and editing game history instances. + """ serializer_class = GameHistorySerializer + queryset = GameHistory.objects.all() - def create_game_history(self, request): - data = request.data - serializer = GameHistorySerializer(data=data) + def create(self, request, *args, **kwargs): + """ + Create a new game history record. + """ + serializer = self.get_serializer(data=request.data) if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def game_history_list(self, request): + def list(self, request, *args, **kwargs): """ - Method to get the list of game history. - - This method gets the list of game history from the database and returns the list of game history. - - Args: - request: The request object. - - Returns: - Response: The response object containing the list of game history. + List all game history records. """ - game_history = GameHistory.objects.all() - serializer = GameHistorySerializer(game_history, many=True) + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - def retrieve_game_history(self, request, pk=None): + def retrieve(self, request, pk=None, *args, **kwargs): """ - Method to retrieve a game history. - - This method retrieves a game history from the database using the game history id and returns the game history data. - - Args: - request: The request object. - pk: The primary key of the game history. - - Returns: - Response: The response object containing the game history data. + Retrieve a specific game history record. """ - data = get_object_or_404(GameHistory, id=pk) - serializer = GameHistorySerializer(data) + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) return Response(serializer.data, status=status.HTTP_200_OK) - def update_game_history(self, request, pk=None): + def update(self, request, pk=None, *args, **kwargs): """ - Method to update a game history. - - This method updates a game history in the database using the game history id and the data in the request. - - Args: - request: The request object containing the game history data. - pk: The primary key of the game history. - - Returns: - Response: The response object containing the updated game history data. + Update a specific game history record. """ - data = get_object_or_404(GameHistory, id=pk) - serializer = GameHistorySerializer(data, data=request.data) + partial = kwargs.pop('partial', False) + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance, data=request.data, partial=partial) if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy_game_history(self, request, pk=None): + def destroy(self, request, pk=None, *args, **kwargs): """ - Method to delete a game history. - - This method deletes a game history from the database using the game history id. - - Args: - request: The request object. - pk: The primary key of the game history. - - Returns: - Response: The response object containing the status of the deletion. + Delete a specific game history record. """ - data = get_object_or_404(GameHistory, id=pk) - data.delete() + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/tests/test_game_history.py index def7e9a..8741849 100644 --- a/Backend/game_history/game_history/tests/test_game_history.py +++ b/Backend/game_history/game_history/tests/test_game_history.py @@ -1,3 +1,52 @@ +# # game_history/tests/test_game_history.py + +# import pytest +# from django.urls import reverse +# from rest_framework.test import APIClient +# from rest_framework import status +# from game_data.models import GameHistory + +# @pytest.fixture +# def api_client(): +# return APIClient() + +# @pytest.mark.django_db +# def test_create_game_history(api_client): +# url = reverse('gamehistory-list') # reverse() is used to generate the URL for the view name 'gamehistory-list' +# data = { +# 'player1': 'player1', +# 'player2': 'player2', +# 'winner': 'player1', +# 'score': '21-15' +# } +# response = api_client.post(url, data, format='json') +# assert response.status_code == status.HTTP_201_CREATED +# assert GameHistory.objects.count() == 1 +# game_history = GameHistory.objects.first() +# assert game_history.player1 == 'player1' +# assert game_history.player2 == 'player2' +# assert game_history.winner == 'player1' +# assert game_history.score == '21-15' + +# @pytest.mark.django_db +# def test_list_game_histories(api_client): +# game1 = GameHistory.objects.create(player1='player1', player2='player2', winner='player1', score='21-15') +# game2 = GameHistory.objects.create(player1='player3', player2='player4', winner='player4', score='19-21') + +# url = reverse('gamehistory-list') +# response = api_client.get(url, format='json') +# assert response.status_code == status.HTTP_200_OK +# assert len(response.data) == 2 +# assert response.data[0]['player1'] == game1.player1 +# assert response.data[0]['player2'] == game1.player2 +# assert response.data[0]['winner'] == game1.winner +# assert response.data[0]['score'] == game1.score +# assert response.data[1]['player1'] == game2.player1 +# assert response.data[1]['player2'] == game2.player2 +# assert response.data[1]['winner'] == game2.winner +# assert response.data[1]['score'] == game2.score + + # game_history/tests/test_game_history.py import pytest @@ -12,36 +61,40 @@ def api_client(): @pytest.mark.django_db def test_create_game_history(api_client): - url = reverse('gamehistory-list') # reverse() is used to generate the URL for the view name 'gamehistory-list' + url = reverse('game-history-list') data = { - 'player1': 'player1', - 'player2': 'player2', - 'winner': 'player1', - 'score': '21-15' + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z', + 'end_time': '2024-07-03T12:30:00Z' } response = api_client.post(url, data, format='json') assert response.status_code == status.HTTP_201_CREATED assert GameHistory.objects.count() == 1 game_history = GameHistory.objects.first() - assert game_history.player1 == 'player1' - assert game_history.player2 == 'player2' - assert game_history.winner == 'player1' - assert game_history.score == '21-15' + assert game_history.player1_id == 1 + assert game_history.player2_id == 2 + assert game_history.winner_id == 1 + assert game_history.start_time.isoformat() == '2024-07-03T12:00:00+00:00' + assert game_history.end_time.isoformat() == '2024-07-03T12:30:00+00:00' @pytest.mark.django_db def test_list_game_histories(api_client): - game1 = GameHistory.objects.create(player1='player1', player2='player2', winner='player1', score='21-15') - game2 = GameHistory.objects.create(player1='player3', player2='player4', winner='player4', score='19-21') + game1 = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time='2024-07-03T12:00:00Z', end_time='2024-07-03T12:30:00Z') + game2 = GameHistory.objects.create(player1_id=3, player2_id=4, winner_id=4, start_time='2024-07-03T13:00:00Z', end_time='2024-07-03T13:30:00Z') - url = reverse('gamehistory-list') + url = reverse('game-history-list') response = api_client.get(url, format='json') assert response.status_code == status.HTTP_200_OK assert len(response.data) == 2 - assert response.data[0]['player1'] == game1.player1 - assert response.data[0]['player2'] == game1.player2 - assert response.data[0]['winner'] == game1.winner - assert response.data[0]['score'] == game1.score - assert response.data[1]['player1'] == game2.player1 - assert response.data[1]['player2'] == game2.player2 - assert response.data[1]['winner'] == game2.winner - assert response.data[1]['score'] == game2.score + assert response.data[0]['player1_id'] == game1.player1_id + assert response.data[0]['player2_id'] == game1.player2_id + assert response.data[0]['winner_id'] == game1.winner_id + assert response.data[0]['start_time'] == game1.start_time.isoformat() + assert response.data[0]['end_time'] == game1.end_time.isoformat() + assert response.data[1]['player1_id'] == game2.player1_id + assert response.data[1]['player2_id'] == game2.player2_id + assert response.data[1]['winner_id'] == game2.winner_id + assert response.data[1]['start_time'] == game2.start_time.isoformat() + assert response.data[1]['end_time'] == game2.end_time.isoformat() diff --git a/Backend/game_history/game_history/urls.py b/Backend/game_history/game_history/urls.py index 6dc3bdb..cc6435c 100644 --- a/Backend/game_history/game_history/urls.py +++ b/Backend/game_history/game_history/urls.py @@ -1,12 +1,5 @@ -# game_history/urls.py - from django.urls import path, include -from rest_framework.routers import DefaultRouter -from game_data.views import GameHistoryViewSet - -router = DefaultRouter() -router.register(r'game-history', GameHistoryViewSet, basename='gamehistory') urlpatterns = [ - path('', include(router.urls)), + path('', include('game_data.urls')), ] diff --git a/Makefile b/Makefile index 8e7e420..b1a9b80 100644 --- a/Makefile +++ b/Makefile @@ -40,20 +40,6 @@ clean: docker volume prune -f docker network prune -f - -# # Free up the port if it's already allocated -# .PHONY: free-port -# free-port: -# @echo "Checking for allocated port 15672..." -# @PIDS=$$(lsof -ti:15672 || netstat -nlp | grep :15672 | awk '{print $$7}' | cut -d'/' -f1 || ss -tuln | grep :15672 | awk '{print $$6}' | cut -d',' -f2); \ -# if [ -n "$$PIDS" ]; then \ -# echo "Port 15672 is in use by PIDs $$PIDS. Attempting to free it..."; \ -# echo "$$PIDS" | xargs kill -9; \ -# echo "Port 15672 has been freed."; \ -# else \ -# echo "Port 15672 is not in use."; \ -# fi - # Display the status of all services .PHONY: status status: @@ -73,6 +59,5 @@ help: @echo " logs - Show logs for all services" @echo " pull - Pull latest images for all services" @echo " clean - Remove stopped containers and unused images, networks, and volumes" - @echo " free-port - Free up the port if it's already allocated" @echo " status - Display the status of all services" @echo " help - Display this help message" From 0918ee8dccea4eed1af2f1ff958470d7b7a11087 Mon Sep 17 00:00:00 2001 From: abbastoof Date: Thu, 4 Jul 2024 17:50:38 +0300 Subject: [PATCH 03/15] Add GameHistory microservice: - Updated GameHistory model - Develop GameHistoryViewSet with CRUD operations - Define URL patterns for game history endpoints - Write and pass tests for creating, listing, retrieving, updating, and deleting game history records - Fix timezone issues in tests - Update documentation for GameHistory microservice --- Backend/game_history/Dockerfile | 21 ++- Backend/game_history/README.md | 130 +++++++++++++++ .../game_history/game_data/rabbitmq_utils.py | 44 ------ Backend/game_history/game_data/views.py | 149 ------------------ Backend/game_history/game_history/consumer.py | 16 -- .../{ => game_history}/game_data/__init__.py | 0 .../{ => game_history}/game_data/admin.py | 0 .../{ => game_history}/game_data/apps.py | 0 .../game_data/migrations/__init__.py | 0 .../{ => game_history}/game_data/models.py | 6 +- .../game_data/serializers.py | 0 .../{ => game_history}/game_data/tests.py | 0 .../{ => game_history}/game_data/urls.py | 0 .../game_history/game_data/views.py | 59 +++++++ .../{ => game_history}/__init__.py | 0 .../game_history/{ => game_history}/asgi.py | 0 .../{ => game_history}/settings.py | 52 +++--- .../game_history/tests/__init__.py | 0 .../{ => game_history}/tests/conftest.py | 5 +- .../game_history/tests/test_game_history.py | 105 ++++++++++++ .../game_history/game_history/urls.py | 7 + .../game_history/{ => game_history}/wsgi.py | 0 .../game_history/tests/test_game_history.py | 100 ------------ Backend/game_history/game_history/urls.py | 5 - Backend/game_history/init_database.sh | 0 Backend/game_history/requirements.txt | 2 + Backend/game_history/run_consumer.sh | 17 -- Backend/game_history/supervisord.conf | 8 - Backend/game_history/tools.sh | 91 ++++++++++- docker-compose.yml | 0 pytest.ini | 0 31 files changed, 427 insertions(+), 390 deletions(-) mode change 100644 => 100755 Backend/game_history/Dockerfile create mode 100644 Backend/game_history/README.md delete mode 100644 Backend/game_history/game_data/rabbitmq_utils.py delete mode 100644 Backend/game_history/game_data/views.py delete mode 100644 Backend/game_history/game_history/consumer.py rename Backend/game_history/{ => game_history}/game_data/__init__.py (100%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/admin.py (100%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/apps.py (100%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/migrations/__init__.py (100%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/models.py (61%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/serializers.py (100%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/tests.py (100%) mode change 100644 => 100755 rename Backend/game_history/{ => game_history}/game_data/urls.py (100%) mode change 100644 => 100755 create mode 100755 Backend/game_history/game_history/game_data/views.py rename Backend/game_history/game_history/{ => game_history}/__init__.py (100%) mode change 100644 => 100755 rename Backend/game_history/game_history/{ => game_history}/asgi.py (100%) mode change 100644 => 100755 rename Backend/game_history/game_history/{ => game_history}/settings.py (83%) mode change 100644 => 100755 create mode 100755 Backend/game_history/game_history/game_history/tests/__init__.py rename Backend/game_history/game_history/{ => game_history}/tests/conftest.py (97%) mode change 100644 => 100755 create mode 100755 Backend/game_history/game_history/game_history/tests/test_game_history.py create mode 100755 Backend/game_history/game_history/game_history/urls.py rename Backend/game_history/game_history/{ => game_history}/wsgi.py (100%) mode change 100644 => 100755 delete mode 100644 Backend/game_history/game_history/tests/test_game_history.py delete mode 100644 Backend/game_history/game_history/urls.py mode change 100644 => 100755 Backend/game_history/init_database.sh mode change 100644 => 100755 Backend/game_history/requirements.txt delete mode 100644 Backend/game_history/run_consumer.sh mode change 100644 => 100755 Backend/game_history/supervisord.conf mode change 100644 => 100755 Backend/game_history/tools.sh mode change 100644 => 100755 docker-compose.yml mode change 100644 => 100755 pytest.ini diff --git a/Backend/game_history/Dockerfile b/Backend/game_history/Dockerfile old mode 100644 new mode 100755 index a798e14..62f66a1 --- a/Backend/game_history/Dockerfile +++ b/Backend/game_history/Dockerfile @@ -2,7 +2,6 @@ FROM alpine:3.20 ENV PYTHONUNBUFFERED=1 ENV LANG=C.UTF-8 -ENV PYTHONPATH=/app:/app/game_data:/app/game_history # Update and install dependencies # trunk-ignore(hadolint/DL3018) @@ -26,11 +25,9 @@ RUN python3 -m venv venv && chown -R postgres:postgres venv COPY --chown=postgres:postgres ./Backend/game_history/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY --chown=postgres:postgres ./Backend/game_history/requirements.txt . COPY --chown=postgres:postgres ./Backend/game_history/game_history /app/game_history -COPY --chown=postgres:postgres ./Backend/game_history/game_data /app/game_data COPY --chown=postgres:postgres ./Backend/game_history/tools.sh /app COPY --chown=postgres:postgres ./Backend/game_history/init_database.sh /app -COPY --chown=postgres:postgres ./Backend/game_history/run_consumer.sh /app -RUN chmod +x /app/tools.sh /app/init_database.sh /app/run_consumer.sh + # Install Python packages RUN . venv/bin/activate && pip install -r requirements.txt @@ -40,18 +37,18 @@ RUN touch /var/log/django.log /var/log/django.err /var/log/init_database.log /va chown -R postgres:postgres /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err # Ensure supervisord and scripts are executable and owned by postgres -RUN chown -R postgres:postgres /etc/supervisor && \ - chown -R postgres:postgres /usr/bin/supervisord && \ - chown -R postgres:postgres /etc/supervisor/conf.d/supervisord.conf && \ - chown -R postgres:postgres /app && \ +# RUN chown -R postgres:postgres /etc/supervisor && \ +# chown -R postgres:postgres /usr/bin/supervisord && \ +# chown -R postgres:postgres /etc/supervisor/conf.d/supervisord.conf && \ +RUN chown -R postgres:postgres /app && \ chown -R postgres:postgres /var/log && \ chown -R postgres:postgres /app/venv && \ - chown -R postgres:postgres /app/game_history && \ - chown -R postgres:postgres /app/game_data && \ - chmod +x /usr/bin/supervisord + chown -R postgres:postgres /app/game_history + # chown -R postgres:postgres /app/game_data + # chmod +x /usr/bin/supervisord USER postgres HEALTHCHECK --interval=30s --timeout=2s --start-period=5s --retries=3 CMD curl -sSf http://localhost:8002/game-history/ > /dev/null && echo "success" || echo "failure" -ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +ENTRYPOINT ["sh", "./tools.sh"] diff --git a/Backend/game_history/README.md b/Backend/game_history/README.md new file mode 100644 index 0000000..b4f2799 --- /dev/null +++ b/Backend/game_history/README.md @@ -0,0 +1,130 @@ +## Game History Microservice Documentation + +### Overview + +The Game History microservice is a key component of the Transcendence project at 42 School. This service is responsible for recording and managing the history of ping pong games played by users. It supports creating, retrieving, updating, and deleting game history records. + +### Directory Structure + +``` +. +├── Dockerfile +├── game_history +│ ├── game_data +│ │ ├── __init__.py +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── migrations +│ │ │ └── __init__.py +│ │ ├── models.py +│ │ ├── serializers.py +│ │ ├── tests.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── game_history +│ │ ├── __init__.py +│ │ ├── asgi.py +│ │ ├── settings.py +│ │ ├── tests +│ │ │ ├── __init__.py +│ │ │ ├── conftest.py +│ │ │ └── test_game_history.py +│ │ ├── urls.py +│ │ └── wsgi.py +│ └── manage.py +├── init_database.sh +├── requirements.txt +├── supervisord.conf +└── tools.sh +``` + +### Setup + +#### Docker Setup + +The `game_history` microservice is containerized using Docker. The `Dockerfile` sets up the environment needed to run the Django application. + +#### Environment Variables + +Environment variables should be defined in the `.env` file to configure the service. These may include database connection details, secret keys, and other configurations. + +### Models + +The `GameHistory` model represents a record of a game played between two users. It includes the following fields: + +- `game_id`: AutoField, primary key. +- `player1_id`: Integer, ID of the first player. +- `player2_id`: Integer, ID of the second player. +- `winner_id`: Integer, ID of the winning player. +- `start_time`: DateTime, the start time of the game. +- `end_time`: DateTime, the end time of the game (auto-populated). + +### Serializers + +The `GameHistorySerializer` converts model instances to JSON format and validates incoming data. + +### Views + +The `GameHistoryViewSet` handles the CRUD operations for game history records. + +### URLs + +The microservice defines several endpoints to interact with the game history data. These endpoints are defined in the `game_data/urls.py` file. Here is an overview of how to access them: + +- **List and Create Game History Records:** + ``` + GET /game-history/ + POST /game-history/ + ``` +- **Retrieve, Update, and Delete a Specific Game History Record:** + ``` + GET /game-history// + PUT /game-history// + DELETE /game-history// + ``` + +### Tests + +#### Directory Structure + +The tests for the Game History microservice are located in the `game_history/game_history/tests/` directory. The tests ensure that the CRUD operations for game history records are working correctly. + +#### Test Cases + +1. **Test Create Game History** + - Verifies that a game history record can be created successfully. + +2. **Test List Game Histories** + - Verifies that a list of game history records can be retrieved successfully. + +3. **Test Retrieve Game History** + - Verifies that a specific game history record can be retrieved successfully. + +4. **Test Update Game History** + - Verifies that a specific game history record can be updated successfully. + +5. **Test Partial Update Game History** + - Verifies that a specific game history record can be partially updated successfully. + +6. **Test Delete Game History** + - Verifies that a specific game history record can be deleted successfully. + +### Running Tests + +To run the tests, use the following commands: + +1. Build and start the Docker container: + ```sh + docker-compose up --build + ``` + +2. Execute the tests within the Docker container: + ```sh + docker exec -it game-history bash + . venv/bin/activate + pytest + ``` + +### Conclusion + +The Game History microservice is an essential part of the Transcendence project, providing a robust solution for managing game history records. This documentation provides an overview of the setup, implementation, and testing of the service. For further details, refer to the respective source code files in the project directory. diff --git a/Backend/game_history/game_data/rabbitmq_utils.py b/Backend/game_history/game_data/rabbitmq_utils.py deleted file mode 100644 index d9bdbb2..0000000 --- a/Backend/game_history/game_data/rabbitmq_utils.py +++ /dev/null @@ -1,44 +0,0 @@ -import pika # RabbitMQ library -import json -from django.conf import settings - -class RabbitMQManager: - _connection = None # Connection to RabbitMQ server is none by default so that we can check if it is connected or not - - @classmethod # Class method to connect to RabbitMQ server - def get_connection(cls): - if not cls._connection or cls._connection.is_closed: # If connection is not established or connection is closed - cls._connection = cls.create_connection() - return cls._connection # Return the connection to RabbitMQ server - - @classmethod # Class method to create connection to RabbitMQ server - def create_connection(cls): - credentials = pika.PlainCredentials(settings.RABBITMQ_USER, settings.RABBITMQ_PASS) # Create credentials to connect to RabbitMQ server - parameters = pika.ConnectionParameters(settings.RABBITMQ_HOST, settings.RABBITMQ_PORT, "/", credentials=credentials) # Create connection parameters to RabbitMQ server - return pika.BlockingConnection(parameters) # Create a blocking connection to RabbitMQ server because we want to wait for the connection to be established before proceeding - - @staticmethod # Static method to publish message to RabbitMQ server - def publish_message(queue_name, message): - connection = RabbitMQManager.get_connection() - channel = connection.channel() # Create a channel to RabbitMQ server to publish message - channel.queue_declare(queue=queue_name, durable=True) # Declare a queue to RabbitMQ server with the given name and make it durable so that it persists even if RabbitMQ server restarts - channel.basic_publish( - exchange='', # Publish message to default exchange - routing_key=queue_name, # Publish message to the given queue - body=json.dumps(message), # Convert message to JSON format before publishing - properties=pika.BasicProperties(delivery_mode=2) # Make the message persistent so that it is not lost even if RabbitMQ server restarts - ) - connection.close() - - @staticmethod # Static method to consume message from RabbitMQ server - def consume_message(queue_name, callback): # Callback function to be called when a message is received - connection = RabbitMQManager.get_connection() - channel = connection.channel() # Create a channel to RabbitMQ server to consume message - channel.queue_declare(queue=queue_name, durable=True) # Declare a queue to RabbitMQ server with the given name and make it durable so that it persists even if RabbitMQ server restarts - - def wrapper(ch, method, properties, body): # Wrapper function to call the callback function with the received message - callback(json.loads(body)) # Convert the received message from JSON format before calling the callback function - ch.basic_ack(delivery_tag=method.delivery_tag) # Acknowledge the message so that it is removed from the queue - - channel.basic_consume(queue=queue_name, on_message_callback=wrapper) # Consume message from the given queue and call the wrapper function when a message is received - channel.start_consuming() # Start consuming messages from RabbitMQ server diff --git a/Backend/game_history/game_data/views.py b/Backend/game_history/game_data/views.py deleted file mode 100644 index 317a0e1..0000000 --- a/Backend/game_history/game_data/views.py +++ /dev/null @@ -1,149 +0,0 @@ -# # game_data/views.py - -# from rest_framework import viewsets -# from .models import GameHistory -# from .serializers import GameHistorySerializer -# from rest_framework.response import Response -# from django.shortcuts import get_object_or_404 -# from rest_framework import status -# class GameHistoryViewSet(viewsets.ModelViewSet): -# queryset = GameHistory.objects.all() -# serializer_class = GameHistorySerializer - -# def create_game_history(self, request): -# data = request.data -# serializer = GameHistorySerializer(data=data) -# if serializer.is_valid(): -# serializer.save() -# return Response(serializer.data, status=status.HTTP_201_CREATED) -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -# def game_history_list(self, request): -# """ -# Method to get the list of game history. - -# This method gets the list of game history from the database and returns the list of game history. - -# Args: -# request: The request object. - -# Returns: -# Response: The response object containing the list of game history. -# """ -# game_history = GameHistory.objects.all() -# serializer = GameHistorySerializer(game_history, many=True) -# return Response(serializer.data, status=status.HTTP_200_OK) - -# def retrieve_game_history(self, request, pk=None): -# """ -# Method to retrieve a game history. - -# This method retrieves a game history from the database using the game history id and returns the game history data. - -# Args: -# request: The request object. -# pk: The primary key of the game history. - -# Returns: -# Response: The response object containing the game history data. -# """ -# data = get_object_or_404(GameHistory, id=pk) -# serializer = GameHistorySerializer(data) -# return Response(serializer.data, status=status.HTTP_200_OK) - -# def update_game_history(self, request, pk=None): -# """ -# Method to update a game history. - -# This method updates a game history in the database using the game history id and the data in the request. - -# Args: -# request: The request object containing the game history data. -# pk: The primary key of the game history. - -# Returns: -# Response: The response object containing the updated game history data. -# """ -# data = get_object_or_404(GameHistory, id=pk) -# serializer = GameHistorySerializer(data, data=request.data) -# if serializer.is_valid(): -# serializer.save() -# return Response(serializer.data, status=status.HTTP_201_CREATED) -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -# def destroy_game_history(self, request, pk=None): -# """ -# Method to delete a game history. - -# This method deletes a game history from the database using the game history id. - -# Args: -# request: The request object. -# pk: The primary key of the game history. - -# Returns: -# Response: The response object containing the status of the deletion. -# """ -# data = get_object_or_404(GameHistory, id=pk) -# data.delete() -# return Response(status=status.HTTP_204_NO_CONTENT) - -from rest_framework import viewsets, status -from .models import GameHistory -from .serializers import GameHistorySerializer -from rest_framework.response import Response -from django.shortcuts import get_object_or_404 - -class GameHistoryViewSet(viewsets.ModelViewSet): - """ - A viewset for viewing and editing game history instances. - """ - serializer_class = GameHistorySerializer - queryset = GameHistory.objects.all() - - def create(self, request, *args, **kwargs): - """ - Create a new game history record. - """ - serializer = self.get_serializer(data=request.data) - if serializer.is_valid(): - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def list(self, request, *args, **kwargs): - """ - List all game history records. - """ - queryset = self.get_queryset() - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def retrieve(self, request, pk=None, *args, **kwargs): - """ - Retrieve a specific game history record. - """ - instance = get_object_or_404(self.get_queryset(), pk=pk) - serializer = self.get_serializer(instance) - return Response(serializer.data, status=status.HTTP_200_OK) - - def update(self, request, pk=None, *args, **kwargs): - """ - Update a specific game history record. - """ - partial = kwargs.pop('partial', False) - instance = get_object_or_404(self.get_queryset(), pk=pk) - serializer = self.get_serializer(instance, data=request.data, partial=partial) - if serializer.is_valid(): - self.perform_update(serializer) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, pk=None, *args, **kwargs): - """ - Delete a specific game history record. - """ - instance = get_object_or_404(self.get_queryset(), pk=pk) - self.perform_destroy(instance) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_history/game_history/consumer.py b/Backend/game_history/game_history/consumer.py deleted file mode 100644 index 5664e08..0000000 --- a/Backend/game_history/game_history/consumer.py +++ /dev/null @@ -1,16 +0,0 @@ -import os - -import django - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "game_history.settings") -django.setup() - - -def start_consumer(): - from users.views import UserViewSet - - UserViewSet().start_consumer() - - -if __name__ == "__main__": - start_consumer() diff --git a/Backend/game_history/game_data/__init__.py b/Backend/game_history/game_history/game_data/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/__init__.py rename to Backend/game_history/game_history/game_data/__init__.py diff --git a/Backend/game_history/game_data/admin.py b/Backend/game_history/game_history/game_data/admin.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/admin.py rename to Backend/game_history/game_history/game_data/admin.py diff --git a/Backend/game_history/game_data/apps.py b/Backend/game_history/game_history/game_data/apps.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/apps.py rename to Backend/game_history/game_history/game_data/apps.py diff --git a/Backend/game_history/game_data/migrations/__init__.py b/Backend/game_history/game_history/game_data/migrations/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/migrations/__init__.py rename to Backend/game_history/game_history/game_data/migrations/__init__.py diff --git a/Backend/game_history/game_data/models.py b/Backend/game_history/game_history/game_data/models.py old mode 100644 new mode 100755 similarity index 61% rename from Backend/game_history/game_data/models.py rename to Backend/game_history/game_history/game_data/models.py index 3f71eb7..d49f264 --- a/Backend/game_history/game_data/models.py +++ b/Backend/game_history/game_history/game_data/models.py @@ -3,9 +3,9 @@ class GameHistory(models.Model): game_id = models.AutoField(primary_key=True) - player1_id = models.IntegerField() # Reference to UserID in UserDB - player2_id = models.IntegerField() # Reference to UserID in UserDB - winner_id = models.IntegerField() # Reference to UserID in UserDB + player1_id = models.IntegerField(unique=True) + player2_id = models.IntegerField(unique=True) + winner_id = models.IntegerField() start_time = models.DateTimeField() end_time = models.DateTimeField(auto_now_add=True) diff --git a/Backend/game_history/game_data/serializers.py b/Backend/game_history/game_history/game_data/serializers.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/serializers.py rename to Backend/game_history/game_history/game_data/serializers.py diff --git a/Backend/game_history/game_data/tests.py b/Backend/game_history/game_history/game_data/tests.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/tests.py rename to Backend/game_history/game_history/game_data/tests.py diff --git a/Backend/game_history/game_data/urls.py b/Backend/game_history/game_history/game_data/urls.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_data/urls.py rename to Backend/game_history/game_history/game_data/urls.py diff --git a/Backend/game_history/game_history/game_data/views.py b/Backend/game_history/game_history/game_data/views.py new file mode 100755 index 0000000..3404c63 --- /dev/null +++ b/Backend/game_history/game_history/game_data/views.py @@ -0,0 +1,59 @@ +from rest_framework import viewsets, status +from .models import GameHistory +from .serializers import GameHistorySerializer +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 + +class GameHistoryViewSet(viewsets.ModelViewSet): + """ + A viewset for viewing and editing game history instances. + """ + serializer_class = GameHistorySerializer + queryset = GameHistory.objects.all() + + def create(self, request, *args, **kwargs): + """ + Create a new game history record. + """ + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request, *args, **kwargs): + """ + List all game history records. + """ + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) # serializer.data is a list of dictionaries containing the serialized data + + def retrieve(self, request, pk=None, *args, **kwargs): + """ + Retrieve a specific game history record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request, pk=None, *args, **kwargs): + """ + Update a specific game history record. + """ + partial = kwargs.pop('partial', False) # this line will remove the 'partial' key from the kwargs dictionary and return its value (False by default), because the partial argument is not needed in the update method + instance = get_object_or_404(self.get_queryset(), pk=pk) # get the instance of the game history record + serializer = self.get_serializer(instance, data=request.data, partial=partial) # create a serializer instance with the instance and the request data as arguments + if serializer.is_valid(): + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, pk=None, *args, **kwargs): + """ + Delete a specific game history record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_history/game_history/__init__.py b/Backend/game_history/game_history/game_history/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_history/__init__.py rename to Backend/game_history/game_history/game_history/__init__.py diff --git a/Backend/game_history/game_history/asgi.py b/Backend/game_history/game_history/game_history/asgi.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_history/asgi.py rename to Backend/game_history/game_history/game_history/asgi.py diff --git a/Backend/game_history/game_history/settings.py b/Backend/game_history/game_history/game_history/settings.py old mode 100644 new mode 100755 similarity index 83% rename from Backend/game_history/game_history/settings.py rename to Backend/game_history/game_history/game_history/settings.py index f4b3906..7fda6bc --- a/Backend/game_history/game_history/settings.py +++ b/Backend/game_history/game_history/game_history/settings.py @@ -20,6 +20,11 @@ # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ +RABBITMQ_HOST = os.getenv("RABBITMQ_HOST") +RABBITMQ_USER = os.getenv("RABBITMQ_USER") +RABBITMQ_PASS = os.getenv("RABBITMQ_PASS") +RABBITMQ_PORT = os.getenv("RABBITMQ_PORT") + # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%" @@ -32,12 +37,9 @@ '[::1]', 'game-history', 'game-history:8002', + 'testserver', ] -RABBITMQ_HOST = os.getenv("RABBITMQ_HOST") -RABBITMQ_USER = os.getenv("RABBITMQ_USER") -RABBITMQ_PASS = os.getenv("RABBITMQ_PASS") -RABBITMQ_PORT = os.getenv("RABBITMQ_PORT") # Application definition @@ -48,7 +50,6 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'game_history', 'game_data', 'rest_framework', 'rest_framework_simplejwt', @@ -69,22 +70,23 @@ 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), - 'DEFAULT_RENDERER_CLASSES': [ - 'rest_framework.renderers.JSONRenderer', - ], - 'DEFAULT_PARSER_CLASSES': [ - 'rest_framework.parsers.JSONParser', - ], + # 'DEFAULT_RENDERER_CLASSES': [ + # 'rest_framework.renderers.JSONRenderer', + # ], + # 'DEFAULT_PARSER_CLASSES': [ + # 'rest_framework.parsers.JSONParser', + # ], } MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "corsheaders.middleware.CorsMiddleware", ] ROOT_URLCONF = 'game_history.urls' @@ -112,13 +114,12 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'postgres', - 'USER': 'root', - 'PASSWORD': 'root', - 'HOST': 'localhost', - 'PORT': '5432', + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "postgres", + "USER": "root", + "PASSWORD": "root", + "PORT": "5432", } } @@ -167,4 +168,3 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' CORS_ORIGIN_ALLOW_ALL = True - diff --git a/Backend/game_history/game_history/game_history/tests/__init__.py b/Backend/game_history/game_history/game_history/tests/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/Backend/game_history/game_history/tests/conftest.py b/Backend/game_history/game_history/game_history/tests/conftest.py old mode 100644 new mode 100755 similarity index 97% rename from Backend/game_history/game_history/tests/conftest.py rename to Backend/game_history/game_history/game_history/tests/conftest.py index d9efd09..2a19453 --- a/Backend/game_history/game_history/tests/conftest.py +++ b/Backend/game_history/game_history/game_history/tests/conftest.py @@ -13,8 +13,8 @@ "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "postgres", - "USER": "postgres", - "PASSWORD": "postgres", + "USER": "root", + "PASSWORD": "root", "PORT": "5432", "HOST": "localhost", "ATOMIC_REQUESTS": True, @@ -29,7 +29,6 @@ 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', - 'game_history', 'game_data', ], MIDDLEWARE=[ diff --git a/Backend/game_history/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/game_history/tests/test_game_history.py new file mode 100755 index 0000000..33cfb08 --- /dev/null +++ b/Backend/game_history/game_history/game_history/tests/test_game_history.py @@ -0,0 +1,105 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient +from rest_framework import status +from game_data.models import GameHistory +from django.utils.timezone import now +from datetime import datetime + +@pytest.fixture +def api_client(): + return APIClient() + +@pytest.mark.django_db +def test_create_game_history(api_client): + url = reverse('game-history-list') # reverse() is used to generate the URL for the 'game-history-list' view + data = { + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z' + } + response = api_client.post(url, data, format='json') # send a POST request to the URL with the data as the request body + assert response.status_code == status.HTTP_201_CREATED + assert GameHistory.objects.count() == 1 # Objects count should be 1 after creating a new GameHistory object in the database + game_history = GameHistory.objects.first() # Get the first GameHistory object from the database + assert game_history.player1_id == 1 + assert game_history.player2_id == 2 + assert game_history.winner_id == 1 + assert game_history.start_time.isoformat() == '2024-07-03T12:00:00+00:00' + assert game_history.end_time is not None + +@pytest.mark.django_db +def test_list_game_histories(api_client): + game1 = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game2 = GameHistory.objects.create(player1_id=3, player2_id=4, winner_id=4, start_time=now()) + + url = reverse('game-history-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 # The response should contain two GameHistory objects in the data field because we created two GameHistory objects in the database + assert response.data[0]['player1_id'] == game1.player1_id + assert response.data[0]['player2_id'] == game1.player2_id + assert response.data[0]['winner_id'] == game1.winner_id + + start_time_response = datetime.fromisoformat(response.data[0]['start_time'].replace('Z', '+00:00')) + start_time_expected = game1.start_time + assert start_time_response == start_time_expected + + assert response.data[1]['player1_id'] == game2.player1_id + assert response.data[1]['player2_id'] == game2.player2_id + assert response.data[1]['winner_id'] == game2.winner_id + + start_time_response = datetime.fromisoformat(response.data[1]['start_time'].replace('Z', '+00:00')) + start_time_expected = game2.start_time + assert start_time_response == start_time_expected + +@pytest.mark.django_db +def test_retrieve_game_history(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + + url = reverse('game-history-detail', args=[game.pk]) + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['player1_id'] == game.player1_id + assert response.data['player2_id'] == game.player2_id + assert response.data['winner_id'] == game.winner_id + assert response.data['start_time'] == game.start_time.isoformat().replace('+00:00', 'Z') + +@pytest.mark.django_db +def test_update_game_history(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + + url = reverse('game-history-detail', args=[game.pk]) + data = { + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 2, + 'start_time': game.start_time.isoformat().replace('+00:00', 'Z') + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + game.refresh_from_db() + assert game.winner_id == 2 + +@pytest.mark.django_db +def test_delete_game_history(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + + url = reverse('game-history-detail', args=[game.pk]) + response = api_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert GameHistory.objects.count() == 0 + +@pytest.mark.django_db +def test_create_game_history_validation_error(api_client): + url = reverse('game-history-list') + data = { + 'player1_id': 1, + # 'player2_id' is missing + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z' + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert 'player2_id' in response.data diff --git a/Backend/game_history/game_history/game_history/urls.py b/Backend/game_history/game_history/game_history/urls.py new file mode 100755 index 0000000..5c561c0 --- /dev/null +++ b/Backend/game_history/game_history/game_history/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("game_data.urls")), +] diff --git a/Backend/game_history/game_history/wsgi.py b/Backend/game_history/game_history/game_history/wsgi.py old mode 100644 new mode 100755 similarity index 100% rename from Backend/game_history/game_history/wsgi.py rename to Backend/game_history/game_history/game_history/wsgi.py diff --git a/Backend/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/tests/test_game_history.py deleted file mode 100644 index 8741849..0000000 --- a/Backend/game_history/game_history/tests/test_game_history.py +++ /dev/null @@ -1,100 +0,0 @@ -# # game_history/tests/test_game_history.py - -# import pytest -# from django.urls import reverse -# from rest_framework.test import APIClient -# from rest_framework import status -# from game_data.models import GameHistory - -# @pytest.fixture -# def api_client(): -# return APIClient() - -# @pytest.mark.django_db -# def test_create_game_history(api_client): -# url = reverse('gamehistory-list') # reverse() is used to generate the URL for the view name 'gamehistory-list' -# data = { -# 'player1': 'player1', -# 'player2': 'player2', -# 'winner': 'player1', -# 'score': '21-15' -# } -# response = api_client.post(url, data, format='json') -# assert response.status_code == status.HTTP_201_CREATED -# assert GameHistory.objects.count() == 1 -# game_history = GameHistory.objects.first() -# assert game_history.player1 == 'player1' -# assert game_history.player2 == 'player2' -# assert game_history.winner == 'player1' -# assert game_history.score == '21-15' - -# @pytest.mark.django_db -# def test_list_game_histories(api_client): -# game1 = GameHistory.objects.create(player1='player1', player2='player2', winner='player1', score='21-15') -# game2 = GameHistory.objects.create(player1='player3', player2='player4', winner='player4', score='19-21') - -# url = reverse('gamehistory-list') -# response = api_client.get(url, format='json') -# assert response.status_code == status.HTTP_200_OK -# assert len(response.data) == 2 -# assert response.data[0]['player1'] == game1.player1 -# assert response.data[0]['player2'] == game1.player2 -# assert response.data[0]['winner'] == game1.winner -# assert response.data[0]['score'] == game1.score -# assert response.data[1]['player1'] == game2.player1 -# assert response.data[1]['player2'] == game2.player2 -# assert response.data[1]['winner'] == game2.winner -# assert response.data[1]['score'] == game2.score - - -# game_history/tests/test_game_history.py - -import pytest -from django.urls import reverse -from rest_framework.test import APIClient -from rest_framework import status -from game_data.models import GameHistory - -@pytest.fixture -def api_client(): - return APIClient() - -@pytest.mark.django_db -def test_create_game_history(api_client): - url = reverse('game-history-list') - data = { - 'player1_id': 1, - 'player2_id': 2, - 'winner_id': 1, - 'start_time': '2024-07-03T12:00:00Z', - 'end_time': '2024-07-03T12:30:00Z' - } - response = api_client.post(url, data, format='json') - assert response.status_code == status.HTTP_201_CREATED - assert GameHistory.objects.count() == 1 - game_history = GameHistory.objects.first() - assert game_history.player1_id == 1 - assert game_history.player2_id == 2 - assert game_history.winner_id == 1 - assert game_history.start_time.isoformat() == '2024-07-03T12:00:00+00:00' - assert game_history.end_time.isoformat() == '2024-07-03T12:30:00+00:00' - -@pytest.mark.django_db -def test_list_game_histories(api_client): - game1 = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time='2024-07-03T12:00:00Z', end_time='2024-07-03T12:30:00Z') - game2 = GameHistory.objects.create(player1_id=3, player2_id=4, winner_id=4, start_time='2024-07-03T13:00:00Z', end_time='2024-07-03T13:30:00Z') - - url = reverse('game-history-list') - response = api_client.get(url, format='json') - assert response.status_code == status.HTTP_200_OK - assert len(response.data) == 2 - assert response.data[0]['player1_id'] == game1.player1_id - assert response.data[0]['player2_id'] == game1.player2_id - assert response.data[0]['winner_id'] == game1.winner_id - assert response.data[0]['start_time'] == game1.start_time.isoformat() - assert response.data[0]['end_time'] == game1.end_time.isoformat() - assert response.data[1]['player1_id'] == game2.player1_id - assert response.data[1]['player2_id'] == game2.player2_id - assert response.data[1]['winner_id'] == game2.winner_id - assert response.data[1]['start_time'] == game2.start_time.isoformat() - assert response.data[1]['end_time'] == game2.end_time.isoformat() diff --git a/Backend/game_history/game_history/urls.py b/Backend/game_history/game_history/urls.py deleted file mode 100644 index cc6435c..0000000 --- a/Backend/game_history/game_history/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import path, include - -urlpatterns = [ - path('', include('game_data.urls')), -] diff --git a/Backend/game_history/init_database.sh b/Backend/game_history/init_database.sh old mode 100644 new mode 100755 diff --git a/Backend/game_history/requirements.txt b/Backend/game_history/requirements.txt old mode 100644 new mode 100755 index 9cd5e52..7a288e0 --- a/Backend/game_history/requirements.txt +++ b/Backend/game_history/requirements.txt @@ -30,5 +30,7 @@ sqlparse==0.5.0; python_version >= '3.8' tomli==2.0.1; python_version <= '3.12' typing-extensions==4.12.1; python_version <= '3.12' # gunicorn==20.1.0; python_version <= '3.12' +pytest-django>=4.4.0 django-environ + daphne diff --git a/Backend/game_history/run_consumer.sh b/Backend/game_history/run_consumer.sh deleted file mode 100644 index 364455c..0000000 --- a/Backend/game_history/run_consumer.sh +++ /dev/null @@ -1,17 +0,0 @@ -# Backend/game_history/run_consumer.sh -#!/bin/bash - -# URL of the Django application -DJANGO_URL="http://localhost:8002/some-endpoint/" # Adjust the endpoint as necessary - -# Wait until Django server is available -while ! curl -s "${DJANGO_URL}" >/dev/null; do - echo "Waiting for Django server at ${DJANGO_URL}..." - sleep 5 -done - -# Activate the virtual environment and start the consumer -source venv/bin/activate -python /app/game_history/consumer.py -echo "Django server is up at ${DJANGO_URL}" -exec "$@" diff --git a/Backend/game_history/supervisord.conf b/Backend/game_history/supervisord.conf old mode 100644 new mode 100755 index ed777af..b69d57a --- a/Backend/game_history/supervisord.conf +++ b/Backend/game_history/supervisord.conf @@ -1,14 +1,6 @@ [supervisord] nodaemon=true -# [program:django] -# command=/app/venv/bin/python /app/manage.py runserver 0.0.0.0:8002 -# user=postgres -# autostart=true -# autorestart=true -# stdout_logfile=/var/log/django.log -# stderr_logfile=/var/log/django.err - [program:django] command=/app/venv/bin/python /app/game_history/manage.py runserver 0.0.0.0:8000 user=postgres diff --git a/Backend/game_history/tools.sh b/Backend/game_history/tools.sh old mode 100644 new mode 100755 index 65bf2f7..05d5ea6 --- a/Backend/game_history/tools.sh +++ b/Backend/game_history/tools.sh @@ -1,27 +1,104 @@ +# # #!/bin/bash + +# # # Run the initialization script +# # sh /app/init_database.sh + +# # # Activate the virtual environment and install dependencies +# # source venv/bin/activate +# # pip install -r requirements.txt +# # pip install tzdata + +# # # Wait for PostgreSQL to be available +# # while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do +# # echo >&2 "Postgres is unavailable - sleeping" +# # sleep 5 +# # done + +# # export DJANGO_SETTINGS_MODULE=game_history.settings + +# # # Apply Django migrations + +# # python3 /app/game_history/manage.py makemigrations +# # python3 /app/game_history/manage.py migrate + +# # # Start the Django application +# # cd /app/game_history +# # daphne -b 0.0.0.0 -p 8002 game_history.asgi:application + + +# #!/bin/bash + +# # Initialize the database +# sh /app/init_database.sh + +# # Activate the virtual environment +# source venv/bin/activate +# pip install -r requirements.txt +# pip install tzdata + +# # Wait for PostgreSQL to be available +# while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do +# echo >&2 "Postgres is unavailable - sleeping" +# sleep 5 +# done + +# # Export Django settings and PYTHONPATH +# export DJANGO_SETTINGS_MODULE=game_history.settings +# export PYTHONPATH=/app + +# # Debugging steps +# echo "PYTHONPATH: $PYTHONPATH" +# echo "Contents of /app:" +# ls /app +# echo "Contents of /app/game_history:" +# ls /app/game_history + +# # Apply Django migrations +# python3 /app/game_history/manage.py makemigrations +# python3 /app/game_history/manage.py migrate + +# # Run pytest with explicit PYTHONPATH +# PYTHONPATH=/app pytest -vv + +# # Start the Django application +# cd /app/game_history +# daphne -b 0.0.0.0 -p 8002 game_history.asgi:application + + #!/bin/bash -# Run the initialization script +# Initialize the database sh /app/init_database.sh -# Activate the virtual environment and install dependencies +# Activate the virtual environment source venv/bin/activate pip install -r requirements.txt pip install tzdata # Wait for PostgreSQL to be available while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do - echo >&2 "Postgres is unavailable - sleeping" - sleep 5 + echo >&2 "Postgres is unavailable - sleeping" + sleep 5 done +# Export Django settings and PYTHONPATH export DJANGO_SETTINGS_MODULE=game_history.settings +export PYTHONPATH=/app -# Apply Django migrations +# Debugging steps +echo "PYTHONPATH: $PYTHONPATH" +echo "Contents of /app:" +ls /app +echo "Contents of /app/game_history:" +ls /app/game_history -python3 /app/game_history/manage.py makemigrations game_data -python3 /app/game_history/manage.py makemigrations game_history +# Apply Django migrations +python3 /app/game_history/manage.py makemigrations python3 /app/game_history/manage.py migrate +# Run pytest with explicit PYTHONPATH +PYTHONPATH=/app pytest -vv + # Start the Django application cd /app/game_history daphne -b 0.0.0.0 -p 8002 game_history.asgi:application diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 diff --git a/pytest.ini b/pytest.ini old mode 100644 new mode 100755 From 9909e81660d4caad989aa7beac5f43594c173f52 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Fri, 5 Jul 2024 15:34:03 +0300 Subject: [PATCH 04/15] removed __pycache__ and .pytest_cache from folders and also added them to .gitignore file --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index d9f7bfb..c20ae1a 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,12 @@ a.out # Ignore Trunk specific files .trunk/ + + +# Ignore __pycache__ +__pycache__/ + + +# Ignore Pytest cache +pytest_cache/ +.pytest_cache/ From ef1b213a57a5d64e44cb3e1c58180c997e120741 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Fri, 5 Jul 2024 17:03:48 +0300 Subject: [PATCH 05/15] added new service name game_stats_service and I also added the same service to docker-compose.yml this service is depends on rabbitmq and I also added its Dockerfile, init_database.sh, requirements.txt supervisord.conf and tools.sh I configured settings for database and rabbitmq and jw t and also added our app to the applications and defined models and it has a lot of works left for views and other things, at this stage it is incomplete --- Backend/game_stats_service/Dockerfile | 49 ++++++ .../game_stats_service/game_stats/__init__.py | 0 .../game_stats_service/game_stats/admin.py | 3 + .../game_stats_service/game_stats/apps.py | 6 + .../game_stats/migrations/__init__.py | 0 .../game_stats_service/game_stats/models.py | 11 ++ .../game_stats/serializers.py | 7 + .../game_stats_service/game_stats/tests.py | 3 + .../game_stats_service/game_stats/urls.py | 0 .../game_stats_service/game_stats/views.py | 7 + .../game_stats_service/__init__.py | 0 .../game_stats_service/asgi.py | 16 ++ .../game_stats_service/settings.py | 159 ++++++++++++++++++ .../game_stats_service/urls.py | 22 +++ .../game_stats_service/wsgi.py | 16 ++ .../game_stats_service/manage.py | 22 +++ Backend/game_stats_service/init_database.sh | 42 +++++ Backend/game_stats_service/requirements.txt | 24 +++ Backend/game_stats_service/supervisord.conf | 20 +++ Backend/game_stats_service/tools.sh | 37 ++++ docker-compose.yml | 13 ++ 21 files changed, 457 insertions(+) create mode 100644 Backend/game_stats_service/Dockerfile create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/__init__.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/admin.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/apps.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/migrations/__init__.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/models.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/serializers.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/tests.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/urls.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats/views.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/__init__.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/settings.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/urls.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py create mode 100755 Backend/game_stats_service/game_stats_service/manage.py create mode 100644 Backend/game_stats_service/init_database.sh create mode 100644 Backend/game_stats_service/requirements.txt create mode 100644 Backend/game_stats_service/supervisord.conf create mode 100644 Backend/game_stats_service/tools.sh diff --git a/Backend/game_stats_service/Dockerfile b/Backend/game_stats_service/Dockerfile new file mode 100644 index 0000000..b8c2f2f --- /dev/null +++ b/Backend/game_stats_service/Dockerfile @@ -0,0 +1,49 @@ +FROM alpine:3.20 + +ENV PYTHONUNBUFFERED=1 +ENV LANG=C.UTF-8 + +# Update and install dependencies + +RUN apk update && apk add --no-cache python3 py3-pip \ + postgresql16 postgresql16-client \ + bash supervisor curl openssl bash \ + build-base libffi-dev python3-dev + +# Set work directory +RUN mkdir /run/postgresql && \ + chown postgres:postgres /run/postgresql && \ + mkdir /app && chown -R postgres:postgres /app + +WORKDIR /app/ + +# Install Python virtual environment +RUN python3 -m venv venv && chown -R postgres:postgres venv + +# Copy application code and adjust permissions +COPY --chown=postgres:postgres ./Backend/game_stats_service/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY --chown=postgres:postgres ./Backend/game_stats_service/requirements.txt . +COPY --chown=postgres:postgres ./Backend/game_stats_service/game_stats_service /app/game_stats_service +COPY --chown=postgres:postgres ./Backend/game_stats_service/tools.sh /app +COPY --chown=postgres:postgres ./Backend/game_stats_service/init_database.sh /app + +# Install Python packages +RUN . venv/bin/activate && pip install -r requirements.txt + +# Create log files and set permissions +RUN touch /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err && \ + chown -R postgres:postgres /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err + +# Ensure supervisord and scripts are executable and owned by postgres +RUN chown -R postgres:postgres /app && \ + chown -R postgres:postgres /var/log && \ + chown -R postgres:postgres /app/venv && \ + chown -R postgres:postgres /app/game_stats_service + +USER postgres + +HEALTHCHECK --interval=30s --timeout=2s --start-period=5s --retries=3 CMD curl -sSf http://localhost:8003/game-stats/ > /dev/null && echo "success" || echo "failure" + +ENTRYPOINT ["sh", "./tools.sh"] + + diff --git a/Backend/game_stats_service/game_stats_service/game_stats/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats/admin.py b/Backend/game_stats_service/game_stats_service/game_stats/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Backend/game_stats_service/game_stats_service/game_stats/apps.py b/Backend/game_stats_service/game_stats_service/game_stats/apps.py new file mode 100644 index 0000000..1d3853c --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GameStatsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'game_stats' diff --git a/Backend/game_stats_service/game_stats_service/game_stats/migrations/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats/models.py b/Backend/game_stats_service/game_stats_service/game_stats/models.py new file mode 100644 index 0000000..c97554e --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/models.py @@ -0,0 +1,11 @@ +from django.db import models + +class GameStats(models.Model): + game_id = models.AutoField(primary_key=True) + player1_score = models.IntegerField(unique=True) + player2_score = models.IntegerField(unique=True) + total_hits = models.IntegerField() + longest_rally = models.IntegerField() + + def __str__(self): + return f"Game {self.game_id}: {self.player1_score} vs {self.player2_score}" diff --git a/Backend/game_stats_service/game_stats_service/game_stats/serializers.py b/Backend/game_stats_service/game_stats_service/game_stats/serializers.py new file mode 100644 index 0000000..0bdafcf --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import GameStats + +class GameStatsSerializer(serializers.ModelSerializer): + class Meta: + model = GameStats + fields = '__all__' diff --git a/Backend/game_stats_service/game_stats_service/game_stats/tests.py b/Backend/game_stats_service/game_stats_service/game_stats/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Backend/game_stats_service/game_stats_service/game_stats/urls.py b/Backend/game_stats_service/game_stats_service/game_stats/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats/views.py b/Backend/game_stats_service/game_stats_service/game_stats/views.py new file mode 100644 index 0000000..6670bd5 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats/views.py @@ -0,0 +1,7 @@ +from rest_framework import viewsets +from .models import GameStats +from .serializers import GameStatsSerializer + +class GameStatsViewSet(viewsets.ModelViewSet): + queryset = GameStats.objects.all() + serializer_class = GameStatsSerializer diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py b/Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py new file mode 100644 index 0000000..51b8c15 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for game_stats_service project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + +application = get_asgi_application() diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/settings.py b/Backend/game_stats_service/game_stats_service/game_stats_service/settings.py new file mode 100644 index 0000000..75cb2ed --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/settings.py @@ -0,0 +1,159 @@ +""" +Django settings for game_stats_service project. + +Generated by 'django-admin startproject' using Django 5.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path +from datetime import timedelta +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-u7!$h!m^swv%n2dtm+(ccqjo6q+j2mpqoi@^o=dq#24=*jef42' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + '[::1]', + 'game-stats-service', + 'game-stats-service:8003', + 'testserver', +] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'game_stats', + 'rest_framework', + 'rest_framework_simplejwt', + 'corsheaders', +] + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": False, + "BLACKLIST_AFTER_ROTATION": True, + "AUTH_HEADER_TYPES": ("Bearer",), + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), +} + +# Add REST framework settings for JWT authentication +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} + + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', # Add this line to the MIDDLEWARE list to enable CORS, Cross-Origin Resource Sharing is a mechanism that allows many resources (e.g., fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the resource originated. + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'game_stats_service.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'game_stats_service.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'root', + 'PASSWORD': 'root', + 'PORT': '5432', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py b/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py new file mode 100644 index 0000000..508f1d0 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for game_stats_service project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py b/Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py new file mode 100644 index 0000000..981e094 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for game_stats_service project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + +application = get_wsgi_application() diff --git a/Backend/game_stats_service/game_stats_service/manage.py b/Backend/game_stats_service/game_stats_service/manage.py new file mode 100755 index 0000000..b054a38 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Backend/game_stats_service/init_database.sh b/Backend/game_stats_service/init_database.sh new file mode 100644 index 0000000..ac30c29 --- /dev/null +++ b/Backend/game_stats_service/init_database.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Create necessary directories with appropriate permissions +cd / +mkdir -p /run/postgresql +chown postgres:postgres /run/postgresql/ + +# Initialize the database +initdb -D /var/lib/postgresql/data + +# Switch to the postgres user and run the following commands +mkdir -p /var/lib/postgresql/data +initdb -D /var/lib/postgresql/data # This command initializes the database cluster in the specified directory. + +# Append configurations to pg_hba.conf and postgresql.conf as the postgres user +echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf # This command appends the specified line to the pg_hba.conf file. +echo "listen_addresses='*'" >> /var/lib/postgresql/data/postgresql.conf # This command appends the specified line to the postgresql.conf file. + +# Remove the unix_socket_directories line from postgresql.conf as the postgres user +sed -i "/^unix_socket_directories = /d" /var/lib/postgresql/data/postgresql.conf # Because the unix_socket_directories line is not needed in this context, this command removes it from the postgresql.conf file. + +# Ensure the postgres user owns the data directory +chown -R postgres:postgres /var/lib/postgresql/data + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +exec postgres -D /var/lib/postgresql/data & + +# Wait for PostgreSQL to start (you may need to adjust the sleep time) +sleep 5 + +# Create a new PostgreSQL user and set the password +psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" +psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" + +# Stop the PostgreSQL server after setting the password +pg_ctl stop -D /var/lib/postgresql/data + +sleep 5 + +# Start the PostgreSQL server as the postgres user, keeping it in the foreground +pg_ctl start -D /var/lib/postgresql/data diff --git a/Backend/game_stats_service/requirements.txt b/Backend/game_stats_service/requirements.txt new file mode 100644 index 0000000..a2c5fde --- /dev/null +++ b/Backend/game_stats_service/requirements.txt @@ -0,0 +1,24 @@ +-i https://pypi.org/simple +asgiref==3.8.1; python_version >= '3.8' +django==5.0.6; python_version >= '3.10' +django-cors-headers==4.3.1; python_version >= '3.8' +django-mysql==4.13.0; python_version >= '3.8' +djangorestframework==3.15.1; python_version >= '3.6' +djangorestframework-simplejwt; python_version >= '3.6' +pgsql; python_version >= '3.7' +djangorestframework-simplejwt[crypto]; python_version >= '3.6' +exceptiongroup==1.2.1; python_version <= '3.12' +iniconfig==2.0.0; python_version >= '3.7' +packaging==24.0; python_version >= '3.7' +pika==1.3.2; python_version >= '3.7' +pluggy==1.5.0; python_version >= '3.8' +psycopg2-binary==2.9.9; python_version >= '3.7' +pytest==8.2.1; python_version >= '3.8' +sqlparse==0.5.0; python_version >= '3.8' +tomli==2.0.1; python_version <= '3.12' +typing-extensions==4.12.1; python_version <= '3.12' +# gunicorn==20.1.0; python_version <= '3.12' +pytest-django>=4.4.0 +django-environ + +daphne diff --git a/Backend/game_stats_service/supervisord.conf b/Backend/game_stats_service/supervisord.conf new file mode 100644 index 0000000..159792c --- /dev/null +++ b/Backend/game_stats_service/supervisord.conf @@ -0,0 +1,20 @@ +[supervisord] +nodaemon=true + +[program:django] +command=/app/venv/bin/python /app/game_stats_service/manage.py runserver +user=postgres +autostart=true +autorestart=true +stderr_logfile=/var/log/django.err.log +stdout_logfile=/var/log/django.out.log +user=postgres + +[program:init_database] +command=/bin/bash /app/init_database.sh +user=postgres +autostart=true +autorestart=true +startsecs=0 +stdout_logfile=/var/log/init_database.log +stderr_logfile=/var/log/init_database.err diff --git a/Backend/game_stats_service/tools.sh b/Backend/game_stats_service/tools.sh new file mode 100644 index 0000000..34c5660 --- /dev/null +++ b/Backend/game_stats_service/tools.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Initialize the database +sh /app/init_database.sh + +# Activate the virtual environment +source venv/bin/activate +pip install -r requirements.txt +pip install tzdata + +# Wait for PostgreSQL to be available +while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do + echo >&2 "Postgres is unavailable - sleeping" + sleep 5 +done + +# Export Django settings and PYTHONPATH +export DJANGO_SETTINGS_MODULE=game_stats_service.settings +export PYTHONPATH=/app + +# Debugging steps +echo "PYTHONPATH: $PYTHONPATH" +echo "Contents of /app:" +ls /app +echo "Contents of /app/game_stats_service:" +ls /app/game_stats_service + +# Apply Django migrations +python3 /app/game_stats_service/manage.py makemigrations +python3 /app/game_stats_service/manage.py migrate + +# Run pytest with explicit PYTHONPATH +PYTHONPATH=/app pytest -vv + +# Start the Django application +cd /app/game_stats_service +daphne -b 0.0.0.0 -p 8003 game_stats_service.asgi:application diff --git a/docker-compose.yml b/docker-compose.yml index 0652306..e692f16 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,19 @@ services: - 8002:8002 networks: - transcendence_network + + game-stats-service: + container_name: game-stats-service + image: game-stats-service + build: + context: . + dockerfile: Backend/game_stats_service/Dockerfile + env_file: + - .env + ports: + - 8003:8003 + networks: + - transcendence_network depends_on: - rabbitmq From 6f5fd0640a59185b9efc5040b96126372ec679cd Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 15:19:17 +0300 Subject: [PATCH 06/15] updated App level urls.py, I added tests folder and some tests for game_stats_service and also added conftest, conftest.py is a file that pytest automatically detects and runs before running any tests. This code is used to configure the Django settings and database for the tests. I also changed models and player1_score and player2_score are not unique anymore, I added game_stats_service.settings and test_game_stats_service.py to pytest.ini, I added create, list, retrieve, update and destroy method to the views.py and I changed the name of my models to /game_dataGameStats/ and I also added /django-stubs/ to the requirements.txt --- Backend/game_history/Dockerfile | 5 - .../game_history/game_data/models.py | 7 +- .../game_history/game_history/settings.py | 4 +- .../game_history/tests/test_game_history.py | 12 +- Backend/game_history/requirements.txt | 12 -- Backend/game_history/tools.sh | 67 ----------- .../game_stats_service/game_stats/models.py | 6 +- .../game_stats/serializers.py | 4 +- .../game_stats_service/game_stats/urls.py | 26 +++++ .../game_stats_service/game_stats/views.py | 40 ++++++- .../game_stats_service/tests/__init__.py | 0 .../game_stats_service/tests/conftest.py | 108 ++++++++++++++++++ .../tests/test_game_stats_service.py | 68 +++++++++++ .../game_stats_service/urls.py | 3 +- Backend/game_stats_service/requirements.txt | 1 + pytest.ini | 9 +- 16 files changed, 264 insertions(+), 108 deletions(-) create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/tests/__init__.py create mode 100755 Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py create mode 100644 Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py diff --git a/Backend/game_history/Dockerfile b/Backend/game_history/Dockerfile index 62f66a1..a198860 100755 --- a/Backend/game_history/Dockerfile +++ b/Backend/game_history/Dockerfile @@ -37,15 +37,10 @@ RUN touch /var/log/django.log /var/log/django.err /var/log/init_database.log /va chown -R postgres:postgres /var/log/django.log /var/log/django.err /var/log/init_database.log /var/log/init_database.err # Ensure supervisord and scripts are executable and owned by postgres -# RUN chown -R postgres:postgres /etc/supervisor && \ -# chown -R postgres:postgres /usr/bin/supervisord && \ -# chown -R postgres:postgres /etc/supervisor/conf.d/supervisord.conf && \ RUN chown -R postgres:postgres /app && \ chown -R postgres:postgres /var/log && \ chown -R postgres:postgres /app/venv && \ chown -R postgres:postgres /app/game_history - # chown -R postgres:postgres /app/game_data - # chmod +x /usr/bin/supervisord USER postgres diff --git a/Backend/game_history/game_history/game_data/models.py b/Backend/game_history/game_history/game_data/models.py index d49f264..1d9cc65 100755 --- a/Backend/game_history/game_history/game_data/models.py +++ b/Backend/game_history/game_history/game_data/models.py @@ -3,11 +3,10 @@ class GameHistory(models.Model): game_id = models.AutoField(primary_key=True) - player1_id = models.IntegerField(unique=True) - player2_id = models.IntegerField(unique=True) + player1_id = models.IntegerField() + player2_id = models.IntegerField() winner_id = models.IntegerField() start_time = models.DateTimeField() - end_time = models.DateTimeField(auto_now_add=True) - + end_time = models.DateTimeField(null=True, blank=True) def __str__(self): return f"Game {self.game_id}: {self.player1_id} vs {self.player2_id} - Winner: {self.winner_id}" diff --git a/Backend/game_history/game_history/game_history/settings.py b/Backend/game_history/game_history/game_history/settings.py index 7fda6bc..fb5c915 100755 --- a/Backend/game_history/game_history/game_history/settings.py +++ b/Backend/game_history/game_history/game_history/settings.py @@ -79,6 +79,7 @@ } MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -86,7 +87,6 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "corsheaders.middleware.CorsMiddleware", ] ROOT_URLCONF = 'game_history.urls' @@ -167,4 +167,4 @@ # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -CORS_ORIGIN_ALLOW_ALL = True + diff --git a/Backend/game_history/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/game_history/tests/test_game_history.py index 33cfb08..f9389be 100755 --- a/Backend/game_history/game_history/game_history/tests/test_game_history.py +++ b/Backend/game_history/game_history/game_history/tests/test_game_history.py @@ -22,12 +22,12 @@ def test_create_game_history(api_client): response = api_client.post(url, data, format='json') # send a POST request to the URL with the data as the request body assert response.status_code == status.HTTP_201_CREATED assert GameHistory.objects.count() == 1 # Objects count should be 1 after creating a new GameHistory object in the database - game_history = GameHistory.objects.first() # Get the first GameHistory object from the database - assert game_history.player1_id == 1 - assert game_history.player2_id == 2 - assert game_history.winner_id == 1 - assert game_history.start_time.isoformat() == '2024-07-03T12:00:00+00:00' - assert game_history.end_time is not None + game_history = GameHistory.objects.first() # Get the first GameHistory object from the database (there should be only one) because we just created it + assert game_history.player1_id == 1 # The player1_id should be 1 because we set it to 1 in the data + assert game_history.player2_id == 2 # The player2_id should be 2 because we set it to 2 in the data + assert game_history.winner_id == 1 # The winner_id should be 1 because we set it to 1 in the data + assert game_history.start_time.isoformat() == '2024-07-03T12:00:00+00:00' # The start_time should be '2024-07-03T12:00:00+00:00' because we set it to that value in the data + assert game_history.end_time is None @pytest.mark.django_db def test_list_game_histories(api_client): diff --git a/Backend/game_history/requirements.txt b/Backend/game_history/requirements.txt index 7a288e0..a2c5fde 100755 --- a/Backend/game_history/requirements.txt +++ b/Backend/game_history/requirements.txt @@ -1,15 +1,3 @@ -# # Backend/game_history/requirements.txt - -# Django>4.2.7 -# djangorestframework>=3.15.2 -# djangorestframework-simplejwt>=4.9.0 -# psycopg2-binary>=2.8.6 -# pika>=1.1.0 -# pytest>=6.2.4 -# pytest-django>=4.4.0 -# django-cors-headers>=4.4.0 -# rest_framework - -i https://pypi.org/simple asgiref==3.8.1; python_version >= '3.8' django==5.0.6; python_version >= '3.10' diff --git a/Backend/game_history/tools.sh b/Backend/game_history/tools.sh index 05d5ea6..1aba6e2 100755 --- a/Backend/game_history/tools.sh +++ b/Backend/game_history/tools.sh @@ -1,70 +1,3 @@ -# # #!/bin/bash - -# # # Run the initialization script -# # sh /app/init_database.sh - -# # # Activate the virtual environment and install dependencies -# # source venv/bin/activate -# # pip install -r requirements.txt -# # pip install tzdata - -# # # Wait for PostgreSQL to be available -# # while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do -# # echo >&2 "Postgres is unavailable - sleeping" -# # sleep 5 -# # done - -# # export DJANGO_SETTINGS_MODULE=game_history.settings - -# # # Apply Django migrations - -# # python3 /app/game_history/manage.py makemigrations -# # python3 /app/game_history/manage.py migrate - -# # # Start the Django application -# # cd /app/game_history -# # daphne -b 0.0.0.0 -p 8002 game_history.asgi:application - - -# #!/bin/bash - -# # Initialize the database -# sh /app/init_database.sh - -# # Activate the virtual environment -# source venv/bin/activate -# pip install -r requirements.txt -# pip install tzdata - -# # Wait for PostgreSQL to be available -# while ! pg_isready -q -U "${DB_USER}" -d "postgres"; do -# echo >&2 "Postgres is unavailable - sleeping" -# sleep 5 -# done - -# # Export Django settings and PYTHONPATH -# export DJANGO_SETTINGS_MODULE=game_history.settings -# export PYTHONPATH=/app - -# # Debugging steps -# echo "PYTHONPATH: $PYTHONPATH" -# echo "Contents of /app:" -# ls /app -# echo "Contents of /app/game_history:" -# ls /app/game_history - -# # Apply Django migrations -# python3 /app/game_history/manage.py makemigrations -# python3 /app/game_history/manage.py migrate - -# # Run pytest with explicit PYTHONPATH -# PYTHONPATH=/app pytest -vv - -# # Start the Django application -# cd /app/game_history -# daphne -b 0.0.0.0 -p 8002 game_history.asgi:application - - #!/bin/bash # Initialize the database diff --git a/Backend/game_stats_service/game_stats_service/game_stats/models.py b/Backend/game_stats_service/game_stats_service/game_stats/models.py index c97554e..c89ce9f 100644 --- a/Backend/game_stats_service/game_stats_service/game_stats/models.py +++ b/Backend/game_stats_service/game_stats_service/game_stats/models.py @@ -1,9 +1,9 @@ from django.db import models -class GameStats(models.Model): +class game_dataGameStats(models.Model): game_id = models.AutoField(primary_key=True) - player1_score = models.IntegerField(unique=True) - player2_score = models.IntegerField(unique=True) + player1_score = models.IntegerField() + player2_score = models.IntegerField() total_hits = models.IntegerField() longest_rally = models.IntegerField() diff --git a/Backend/game_stats_service/game_stats_service/game_stats/serializers.py b/Backend/game_stats_service/game_stats_service/game_stats/serializers.py index 0bdafcf..1762763 100644 --- a/Backend/game_stats_service/game_stats_service/game_stats/serializers.py +++ b/Backend/game_stats_service/game_stats_service/game_stats/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers -from .models import GameStats +from .models import game_dataGameStats class GameStatsSerializer(serializers.ModelSerializer): class Meta: - model = GameStats + model = game_dataGameStats fields = '__all__' diff --git a/Backend/game_stats_service/game_stats_service/game_stats/urls.py b/Backend/game_stats_service/game_stats_service/game_stats/urls.py index e69de29..b6c3d29 100644 --- a/Backend/game_stats_service/game_stats_service/game_stats/urls.py +++ b/Backend/game_stats_service/game_stats_service/game_stats/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from .views import GameStatsViewSet + +urlpatterns = [ + path( + "game-stats/", + GameStatsViewSet.as_view( + { + "get": "list", + "post": "create" + } + ), + name="gamestats-list", + ), + path( + "game-stats//", + GameStatsViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "delete": "destroy", + } + ), + name="gamestats-detail", + ), +] diff --git a/Backend/game_stats_service/game_stats_service/game_stats/views.py b/Backend/game_stats_service/game_stats_service/game_stats/views.py index 6670bd5..ac697cb 100644 --- a/Backend/game_stats_service/game_stats_service/game_stats/views.py +++ b/Backend/game_stats_service/game_stats_service/game_stats/views.py @@ -1,7 +1,41 @@ -from rest_framework import viewsets -from .models import GameStats +from rest_framework import viewsets, status +from .models import game_dataGameStats from .serializers import GameStatsSerializer +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from rest_framework.request import Request class GameStatsViewSet(viewsets.ModelViewSet): - queryset = GameStats.objects.all() + queryset = game_dataGameStats.objects.all() serializer_class = GameStatsSerializer + + def create(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.perform_create(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request: Request, *args: tuple, **kwargs: dict) -> Response: + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request: Request, pk=None, *args: tuple, **kwargs: dict) -> Response: + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request: Request, pk=None, *args: tuple, **kwargs: dict) -> Response: + partial = kwargs.pop('partial', False) + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance, data=request.data, partial=partial) + if serializer.is_valid(): + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request: Request, pk=None, *args: tuple, **kwargs: dict) -> Response: + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/__init__.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py new file mode 100755 index 0000000..17aa9be --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py @@ -0,0 +1,108 @@ +import os +import django +from django.conf import settings +import pytest +from datetime import timedelta + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') + +if not settings.configured: + settings.configure( + DEBUG=True, + DATABASES={ + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "postgres", + "USER": "root", + "PASSWORD": "root", + "PORT": "5432", + "HOST": "localhost", + "ATOMIC_REQUESTS": True, + } + }, + INSTALLED_APPS=[ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'game_stats', + ], + MIDDLEWARE=[ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ], + ROOT_URLCONF='game_stats_service.urls', + TEMPLATES=[ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, + ], + WSGI_APPLICATION='game_history.wsgi.application', + SECRET_KEY='django-insecure-woftd2en2**zr(b%#*2vit2v%s@(k54gb^c(ots0abo7(wsmo%', + ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + '[::1]', + 'game-stats-service', + 'game-stats-service:8003', + ], + RABBITMQ_HOST='localhost', + RABBITMQ_USER='user', + RABBITMQ_PASS='pass', + RABBITMQ_PORT='5672', + REST_FRAMEWORK={ + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + }, + SIMPLE_JWT={ + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_OBTAIN_SERIALIZER': 'user_auth.serializers.CustomTokenObtainPairSerializer', + }, + LANGUAGE_CODE='en-us', + TIME_ZONE='UTC', + USE_I18N=True, + USE_L10N=True, + USE_TZ=True, + STATIC_URL='/static/', + DEFAULT_AUTO_FIELD='django.db.models.BigAutoField', + CORS_ORIGIN_ALLOW_ALL=True, + ) + +django.setup() + +@pytest.fixture(scope='session', autouse=True) +def django_db_setup(): + settings.DATABASES['default'] = { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'postgres', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': 'localhost', + 'PORT': '5432', + 'ATOMIC_REQUESTS': True, + } diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py new file mode 100644 index 0000000..f52e7c5 --- /dev/null +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/test_game_stats_service.py @@ -0,0 +1,68 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient +from game_stats.models import game_dataGameStats + +@pytest.fixture # This decorator is used to create fixtures, which are reusable components that can be used in multiple test functions +def api_client(): + return APIClient() + +@pytest.mark.django_db # This decorator is used to tell pytest to use a Django test database +def test_create_game_stat(api_client): + url = reverse('gamestats-list') + data = { + 'player1_score': 10, + 'player2_score': 8, + 'total_hits': 50, + 'longest_rally': 12 + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert game_dataGameStats.objects.count() == 1 + assert response.data['player1_score'] == 10 + assert response.data['player2_score'] == 8 + +@pytest.mark.django_db +def test_list_game_stats(api_client): + game1 = game_dataGameStats.objects.create(player1_score=10, player2_score=5, total_hits=40, longest_rally=15) + game2 = game_dataGameStats.objects.create(player1_score=20, player2_score=10, total_hits=80, longest_rally=20) + + url = reverse('gamestats-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 # Check if the response contains two game stats objects in the list because we created two game stats objects + +@pytest.mark.django_db +def test_retrieve_game_stat(api_client): + game = game_dataGameStats.objects.create(player1_score=15, player2_score=7, total_hits=60, longest_rally=18) + + url = reverse('gamestats-detail', kwargs={'pk': game.game_id}) + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['player1_score'] == 15 + +@pytest.mark.django_db +def test_update_game_stat(api_client): + game = game_dataGameStats.objects.create(player1_score=10, player2_score=5, total_hits=40, longest_rally=15) + + url = reverse('gamestats-detail', kwargs={'pk': game.game_id}) + data = { + 'player1_score': 20, + 'player2_score': 15, + 'total_hits': 85, + 'longest_rally': 25 + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + game.refresh_from_db() + assert game.player1_score == 20 + +@pytest.mark.django_db +def test_delete_game_stat(api_client): + game = game_dataGameStats.objects.create(player1_score=10, player2_score=5, total_hits=40, longest_rally=15) # Create a game stats object + + url = reverse('gamestats-detail', kwargs={'pk': game.game_id}) # Get the URL for the game stats object + response = api_client.delete(url) # Send a DELETE request to the URL + assert response.status_code == status.HTTP_204_NO_CONTENT # Check if the response status code is 204 (No Content) + assert game_dataGameStats.objects.count() == 0 # Check if the game stats object has been deleted from the database diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py b/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py index 508f1d0..a9a98ec 100644 --- a/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/urls.py @@ -15,8 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('', include('game_stats.urls')), ] diff --git a/Backend/game_stats_service/requirements.txt b/Backend/game_stats_service/requirements.txt index a2c5fde..5330c9d 100644 --- a/Backend/game_stats_service/requirements.txt +++ b/Backend/game_stats_service/requirements.txt @@ -17,6 +17,7 @@ pytest==8.2.1; python_version >= '3.8' sqlparse==0.5.0; python_version >= '3.8' tomli==2.0.1; python_version <= '3.12' typing-extensions==4.12.1; python_version <= '3.12' +django-stubs==1.9.0; python_version >= '3.8' # gunicorn==20.1.0; python_version <= '3.12' pytest-django>=4.4.0 django-environ diff --git a/pytest.ini b/pytest.ini index 3c576a6..022f0a4 100755 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,7 @@ -# pytest.ini [pytest] -DJANGO_SETTINGS_MODULE = game_history.settings -python_files = test_game_history.py +DJANGO_SETTINGS_MODULE = + game_history.settings + game_stats_service.settings +python_files = + test_game_history.py + test_game_stats_service.py From 9afb65c867edeabf83ec772d9165a7912f672512 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 15:33:22 +0300 Subject: [PATCH 07/15] Add README documentation for Game Stats Service microservice - Overview of the Game Stats Service microservice - Directory structure explanation - Setup instructions including Docker setup and environment variables - Description of models, serializers, and views - Endpoint definitions for interacting with game history data - Test directory structure and test case descriptions - Instructions for running tests --- Backend/game_stats_service/README.md | 112 +++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 Backend/game_stats_service/README.md diff --git a/Backend/game_stats_service/README.md b/Backend/game_stats_service/README.md new file mode 100644 index 0000000..7b0ea93 --- /dev/null +++ b/Backend/game_stats_service/README.md @@ -0,0 +1,112 @@ +## Game Stats Service Microservice Documentation + +### Overview + +The Game Stats Service microservice is a crucial part of the Transcendence project at 42 School. This service is designed to handle the recording and management of game statistics for ping pong games played by users. It includes capabilities for creating, retrieving, updating, and deleting game statistics records. + +### Directory Structure + +``` +. +├── Dockerfile +├── game_stats_service +│ ├── game_stats +│ │ ├── admin.py +│ │ ├── apps.py +│ │ ├── __init__.py +│ │ ├── migrations +│ │ │ └── __init__.py +│ │ ├── models.py +│ │ ├── serializers.py +│ │ ├── tests.py +│ │ ├── urls.py +│ │ └── views.py +│ ├── game_stats_service +│ │ ├── asgi.py +│ │ ├── __init__.py +│ │ ├── settings.py +│ │ ├── tests +│ │ │ ├── conftest.py +│ │ │ ├── __init__.py +│ │ │ └── test_game_stats_service.py +│ │ ├── urls.py +│ │ └── wsgi.py +│ └── manage.py +├── init_database.sh +├── requirements.txt +├── supervisord.conf +└── tools.sh +``` + +### Setup + +#### Docker Setup + +The `game_stats_service` microservice is containerized using Docker. The `Dockerfile` sets up the environment required to run the Django application. + +### Models + +The `GameHistory` model represents a record of a game, including details like players, winner, and game timing. + +### Serializers + +The `GameHistorySerializer` converts model instances to JSON format and validates incoming data. + +### Views + +The `GameHistoryViewSet` manages CRUD operations for game history records. + +### URLs + +The microservice defines several endpoints to interact with game history data, specified in the `game_stats/urls.py` file. + +- **List and Create Game History Records:** + ``` + GET /game-history/ + POST /game-history/ + ``` +- **Retrieve, Update, and Delete a Specific Game History Record:** + ``` + GET /game-history// + PUT /game-history// + DELETE /game-history// + ``` + +### Tests + +#### Directory Structure + +The tests for the Game Stats Service microservice are located in the `game_stats_service/game_stats_service/tests/` directory. These tests verify the correct functioning of CRUD operations. + +#### Test Cases + +1. **Test Create Game History** +2. **Test List Game Histories** +3. **Test Retrieve Game History** +4. **Test Update Game History** +5. **Test Delete Game History** +6. **Test Create Game History Validation Error** + +### Running Tests + +To run the tests, follow these steps: + +1. Ensure the Docker containers are up and running: + ```sh + docker-compose up --build + ``` + +2. Access the `game-stats-service` container: + ```sh + docker exec -it game-stats-service bash + ``` + +3. Activate the virtual environment and run the tests: + ```sh + . venv/bin/activate + pytest + ``` + +### Conclusion + +The Game Stats Service microservice is an essential component of the Transcendence project, offering robust functionality for managing game statistics. This documentation provides an overview of the setup, implementation, and testing of the service. For further details, refer to the respective source code files in the project directory. From 1749c4c75764f7c28e9ffc5fb81f099668acd57b Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 15:43:38 +0300 Subject: [PATCH 08/15] updated CI/CD and requirements.txt in root directory and added Dockfile .flake8 files to the root directory of the project --- .flake8 | 3 + .github/workflows/ci.yml | 270 ++++++++++++++++++++++++--------------- Dockerfile | 21 +++ requirements.txt | 63 ++++----- 4 files changed, 224 insertions(+), 133 deletions(-) create mode 100644 .flake8 create mode 100644 Dockerfile diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..209c63e --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = venv/* +max-line-length = 79 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ba24b3..97e4b3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,126 +1,192 @@ -name: CI Pipeline +# name: CI Pipeline + +# permissions: +# pull-requests: read +# contents: read +# issues: read +# deployments: read + +# # Events that trigger the workflow +# on: +# push: +# branches: [main, develop] # Trigger on push to main and develop branches +# pull_request: +# branches: [main, develop] # Trigger on pull request to main and develop branches + +# # Define jobs in the workflow +# jobs: +# setup: +# runs-on: ubuntu-latest +# steps: +# - name: Upgrade setuptools +# run: pip install --upgrade setuptools + +# - name: Checkout code +# uses: actions/checkout@v3 # Checkout the repository code + +# # Set up Python environment +# - name: Set up Python 3.11 +# uses: actions/setup-python@v3 +# with: +# python-version: 3.11 # Use Python version 3.11 + +# # Install project dependencies +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip # Upgrade pip +# pip install -r requirements.txt # Install dependencies from requirements.txt + +# # test: +# # needs: setup +# # runs-on: ubuntu-latest +# # steps: +# # - name: Checkout code +# # uses: actions/checkout@v3 + +# # - name: Set up Python 3.11 +# # uses: actions/setup-python@v3 +# # with: +# # python-version: 3.11 + +# # - name: Install dependencies +# # run: | +# # python -m pip install --upgrade pip +# # pip install -r requirements.txt + +# # # Run test suite +# # - name: Run tests +# # run: | +# # pytest # Execute tests using pytest + +# security: +# needs: setup +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 + +# - name: Set up Python 3.11 +# uses: actions/setup-python@v3 +# with: +# python-version: 3.11 + +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt + +# # Run security checks +# - name: Run security checks +# run: | +# pip install bandit # Install Bandit for security checks +# bandit -r . # Run Bandit on the codebase + +# build: +# needs: [setup, security] +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 + +# - name: Set up Python 3.11 +# uses: actions/setup-python@v3 +# with: +# python-version: 3.11 + +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r requirements.txt + +# # Build the Docker image +# - name: Build Docker image +# run: | +# docker build -t transcendence . + +# deploy: +# needs: build +# runs-on: ubuntu-latest +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 + +# # Log in to Docker Hub +# - name: Deploy to Docker Hub +# env: +# DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} +# DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} +# run: | +# echo "${DOCKER_HUB_PASSWORD}" | docker login -u "${DOCKER_HUB_USERNAME}" --password-stdin +# docker tag transcendence ${DOCKER_HUB_USERNAME}/transcendence:latest +# docker push ${DOCKER_HUB_USERNAME}/transcendence:latest + +# # Deploy to the server +# - name: Deploy to server +# run: | +# ssh user@server "docker pull ${DOCKER_HUB_USERNAME}/transcendence:latest && docker-compose up --build -d" -permissions: - pull-requests: read - contents: read - issues: read - deployments: read +name: CI Pipeline -# Events that trigger the workflow on: push: - branches: [main, develop] # Trigger on push to main and develop branches - pull_request: - branches: [main, develop] # Trigger on pull request to main and develop branches + branches: + - feature/015-game-history-microservice -# Define jobs in the workflow jobs: - setup: + lint-project: runs-on: ubuntu-latest steps: - - name: Upgrade setuptools - run: pip install --upgrade setuptools + - name: Check out code + uses: actions/checkout@v2 - - name: Checkout code - uses: actions/checkout@v3 # Checkout the repository code - - # Set up Python environment - - name: Set up Python 3.11 - uses: actions/setup-python@v3 + - name: Set up Python + uses: actions/setup-python@v2 with: - python-version: 3.11 # Use Python version 3.11 - - # Install project dependencies - - name: Install dependencies - run: | - python -m pip install --upgrade pip # Upgrade pip - pip install -r requirements.txt # Install dependencies from requirements.txt - - # test: - # needs: setup - # runs-on: ubuntu-latest - # steps: - # - name: Checkout code - # uses: actions/checkout@v3 - - # - name: Set up Python 3.11 - # uses: actions/setup-python@v3 - # with: - # python-version: 3.11 - - # - name: Install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r requirements.txt - - # # Run test suite - # - name: Run tests - # run: | - # pytest # Execute tests using pytest - - security: - needs: setup - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: 3.11 + python-version: '3.11' - name: Install dependencies run: | + python -m venv venv + . venv/bin/activate python -m pip install --upgrade pip + pip install setuptools==58.0.4 wheel pip install -r requirements.txt - - # Run security checks - - name: Run security checks + pip install flake8 + - name: Create flake8 configuration file run: | - pip install bandit # Install Bandit for security checks - bandit -r . # Run Bandit on the codebase + echo "[flake8]" > .flake8 + echo "exclude = venv/*" >> .flake8 + echo "max-line-length = 79" >> .flake8 - build: - needs: [setup, security] - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: 3.11 - - - name: Install dependencies + - name: Verify installed packages run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + . venv/bin/activate + pip check - # Build the Docker image - - name: Build Docker image + - name: Run linters run: | - docker build -t transcendence . + . venv/bin/activate + flake8 . - deploy: - needs: build + publish-test-image: + needs: lint-project runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 - - # Log in to Docker Hub - - name: Deploy to Docker Hub - env: - DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} - DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - run: | - echo "${DOCKER_HUB_PASSWORD}" | docker login -u "${DOCKER_HUB_USERNAME}" --password-stdin - docker tag transcendence ${DOCKER_HUB_USERNAME}/transcendence:latest - docker push ${DOCKER_HUB_USERNAME}/transcendence:latest + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 - # Deploy to the server - - name: Deploy to server - run: | - ssh user@server "docker pull ${DOCKER_HUB_USERNAME}/transcendence:latest && docker-compose up --build -d" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: abbastoof + password: ${{ secrets.GH_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + push: true + tags: ghcr.io/${{ github.repository }}:feature/015-game-history-microservice diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d635401 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11 + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip --timeout=1000 install -r requirements.txt + +WORKDIR /app + +COPY . /app + +RUN chown -R www-data:www-data /app + +USER www-data + +EXPOSE 8000 + +CMD ["manage.py", "runserver", "0.0.0.0:8000"] diff --git a/requirements.txt b/requirements.txt index 71b61c8..0db0bdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,35 +1,36 @@ -# # # Application dependencies -# Django==4.0.6 -# djangorestframework==3.12.4 -# fastapi==0.75.1 -# uvicorn==0.17.0 -# mysqlclient==2.0.3 -# requests==2.26.0 +# Application dependencies +Django==4.0.6 +djangorestframework==3.12.4 +fastapi==0.75.1 +uvicorn==0.17.0 +mysqlclient==2.0.3 +requests==2.26.0 -# # # Testing dependencies -# pytest==7.1.2 +# Testing dependencies +pytest==7.1.2 -# # # Security dependencies -# bandit==1.7.0 +# Security dependencies +bandit==1.7.0 -# # # Other dependencies -# docker==5.0.3 -# Pillow==9.0.0 -# django-redis==5.0.0 -# asgiref==3.5.0 -# channels==3.0.4 -# idna==3.3 -# pytz==2021.3 -# python-dateutil==2.8.2 -# simplejson==3.17.5 -# urllib3==1.26.8 -# sqlparse==0.4.2 -# PyYAML==6.0 -# typing_extensions==4.0.1 -# django-rest-swagger==2.2.0 -# django-rest-knox==4.1.0 -# django-rest-auth==0.9.5 -# django-allauth==0.47.0 +# Other dependencies +docker==5.0.3 +Pillow==9.0.0 +django-redis==5.0.0 +asgiref==3.5.0 +channels==3.0.4 +idna==3.3 +pytz==2021.3 +python-dateutil==2.8.2 +simplejson==3.17.5 +urllib3==1.26.8 +sqlparse==0.4.2 +PyYAML==6.0 +typing_extensions==4.0.1 +django-rest-swagger==2.2.0 +django-rest-knox==4.1.0 +django-rest-auth==0.9.5 +django-allauth==0.47.0 -# # # Build dependencies -# setuptools==58.0.4 +# Build dependencies +setuptools==58.0.4 +wheel==0.37.0 From c99e20ba316ca113a36a7e4464fb6b350e93c0bd Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 15:49:08 +0300 Subject: [PATCH 09/15] fixed error from linter: ./Backend/user_management/user_management/users/views.py:182:1: W293 blank line contains whitespace --- .../user_management/users/views.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Backend/user_management/user_management/users/views.py b/Backend/user_management/user_management/users/views.py index ea3d0aa..ccea69d 100644 --- a/Backend/user_management/user_management/users/views.py +++ b/Backend/user_management/user_management/users/views.py @@ -24,7 +24,7 @@ class UserViewSet(viewsets.ViewSet): Attributes: authentication_classes: The list of authentication classes to use for the view. permission_classes: The list of permission classes to use for the view. - + Methods: users_list: Method to get the list of users. retrieve_user: Method to retrieve a user. @@ -32,7 +32,7 @@ class UserViewSet(viewsets.ViewSet): destroy_user: Method to delete a user. handle_rabbitmq_request: Method to handle the RabbitMQ request. start_consumer: Method to start the RabbitMQ consumer. - + """ authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -40,9 +40,9 @@ class UserViewSet(viewsets.ViewSet): def users_list(self, request) -> Response: """ Method to get the list of users. - + This method gets the list of users from the database and returns the list of users. - + Args: request: The request object. @@ -64,7 +64,7 @@ def retrieve_user(self, request, pk=None) -> Response: Args: request: The request object. pk: The primary key of the user. - + Returns: Response: The response object containing the user data. """ @@ -75,13 +75,13 @@ def retrieve_user(self, request, pk=None) -> Response: def update_user(self, request, pk=None) -> Response: """ Method to update a user. - + This method updates a user in the database using the user id and the data in the request. - + Args: request: The request object containing the user data. pk: The primary key of the user. - + Returns: Response: The response object containing the updated user data. """ @@ -97,13 +97,13 @@ def update_user(self, request, pk=None) -> Response: def destroy_user(self, request, pk=None) -> Response: """ Method to delete a user. - + This method deletes a user from the database using the user id. - + Args: request: The request object. pk: The primary key of the user. - + Returns: Response: The response object containing the status of the deletion. """ @@ -121,18 +121,18 @@ def destroy_user(self, request, pk=None) -> Response: def handle_rabbitmq_request(ch, method, properties, body) -> None: """ Method to handle the RabbitMQ request. - + This method handles the RabbitMQ request by authenticating the user and sending the response message. - + Args: ch: The channel object. method: The method object. properties: The properties object. body: The body of the message. - + Returns: None - + """ payload = json.loads(body) username = payload.get("username") @@ -160,7 +160,7 @@ def start_consumer(self) -> None: class RegisterViewSet(viewsets.ViewSet): """ RegisterViewSet class to handle user registration. - + This class inherits from ViewSet. It defines the method to handle user registration. Attributes: @@ -179,7 +179,6 @@ def create_user(self, request) -> Response: Args: request: The request object containing the user data. - Returns: Response: The response object containing the user data. """ From 102b40b2cce036b4453c836fc1c01429e4b8afab Mon Sep 17 00:00:00 2001 From: abbastoof Date: Mon, 8 Jul 2024 18:03:41 +0300 Subject: [PATCH 10/15] fix: Ensure proper database permissions and migration setup - Updated init_database.sh to grant necessary privileges to the PostgreSQL user. - Included ALTER USER and GRANT statements to allow the user to create databases and manage tables, sequences, and functions. - Added migration command in conftest.py to ensure Django migrations are applied before running tests. - Verified database connection and user permissions for successful test execution. - Changed database name to 'test_game_history_db' in the configuration for running tests. --- .../game_history/tests/conftest.py | 7 +++-- Backend/game_history/init_database.sh | 26 ++++++++++++++++--- sample env | 12 +++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 sample env diff --git a/Backend/game_history/game_history/game_history/tests/conftest.py b/Backend/game_history/game_history/game_history/tests/conftest.py index 2a19453..2ed71aa 100755 --- a/Backend/game_history/game_history/game_history/tests/conftest.py +++ b/Backend/game_history/game_history/game_history/tests/conftest.py @@ -3,6 +3,8 @@ from django.conf import settings import pytest from datetime import timedelta +from django.core.management import call_command + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_history.settings') @@ -12,7 +14,7 @@ DATABASES={ "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": "postgres", + "NAME": "test_game_history_db", "USER": "root", "PASSWORD": "root", "PORT": "5432", @@ -93,10 +95,11 @@ def django_db_setup(): settings.DATABASES['default'] = { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'postgres', + 'NAME': 'test_game_history_db', 'USER': 'postgres', 'PASSWORD': 'postgres', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, } + call_command('migrate') diff --git a/Backend/game_history/init_database.sh b/Backend/game_history/init_database.sh index 5a87858..6f964f0 100755 --- a/Backend/game_history/init_database.sh +++ b/Backend/game_history/init_database.sh @@ -28,10 +28,30 @@ exec postgres -D /var/lib/postgresql/data & # Wait for PostgreSQL to start (you may need to adjust the sleep time) sleep 5 -# Create a new PostgreSQL user and set the password +# # Create a new PostgreSQL user and set the password psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" -psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" -psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" +# psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" +# psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" + +# # Create the database named test_game_history_db. +# psql -c "CREATE DATABASE test_game_history_db;" +# psql -c "GRANT ALL PRIVILEGES ON DATABASE test_game_history_db TO ${DB_USER};" + +# Grant all necessary privileges to the user +psql -c "ALTER USER ${DB_USER} CREATEDB;" +psql -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${DB_USER};" + +# Create the database named test_game_history_db. +psql -c "CREATE DATABASE test_game_history_db OWNER ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON DATABASE test_game_history_db TO ${DB_USER};" + + +# Run Django migrations +cd /app +source venv/bin/activate +python manage.py migrate # Stop the PostgreSQL server after setting the password pg_ctl stop -D /var/lib/postgresql/data diff --git a/sample env b/sample env new file mode 100644 index 0000000..11308ae --- /dev/null +++ b/sample env @@ -0,0 +1,12 @@ +DB_USER = "root" +DB_PASS = "root" + +RABBITMQ_DEFAULT_USER="user" +RABBITMQ_DEFAULT_PASS="pass" + +RABBITMQ_HOST="rabbitmq" +RABBITMQ_USER="user" +RABBITMQ_PASS="pass" +RABBITMQ_PORT="5672" + +PGPASSWORD='root' From d1c8dd2026bc83590cb6dc2fb2ea233f7991514e Mon Sep 17 00:00:00 2001 From: abbastoof Date: Mon, 8 Jul 2024 18:15:22 +0300 Subject: [PATCH 11/15] fix: Ensure proper database permissions and migration setup - Updated init_database.sh to grant necessary privileges to the PostgreSQL user. - Included ALTER USER and GRANT statements to allow the user to create databases and manage tables, sequences, and functions. - Added migration command in conftest.py to ensure Django migrations are applied before running tests. - Verified database connection and user permissions for successful test execution. - Changed database name to 'test_game_stats_db' in the configuration for running tests. --- .../game_stats_service/tests/conftest.py | 6 ++++-- Backend/game_stats_service/init_database.sh | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py index 17aa9be..a314d57 100755 --- a/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py +++ b/Backend/game_stats_service/game_stats_service/game_stats_service/tests/conftest.py @@ -3,6 +3,7 @@ from django.conf import settings import pytest from datetime import timedelta +from django.core.management import call_command os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'game_stats_service.settings') @@ -12,7 +13,7 @@ DATABASES={ "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": "postgres", + "NAME": "test_game_stats_db", "USER": "root", "PASSWORD": "root", "PORT": "5432", @@ -99,10 +100,11 @@ def django_db_setup(): settings.DATABASES['default'] = { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'postgres', + 'NAME': 'test_game_stats_db', 'USER': 'postgres', 'PASSWORD': 'postgres', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, } + call_command('migrate') diff --git a/Backend/game_stats_service/init_database.sh b/Backend/game_stats_service/init_database.sh index ac30c29..be77565 100644 --- a/Backend/game_stats_service/init_database.sh +++ b/Backend/game_stats_service/init_database.sh @@ -30,8 +30,23 @@ sleep 5 # Create a new PostgreSQL user and set the password psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" -psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" -psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" +# psql -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" +# psql -c "GRANT ALL PRIVILEGES ON DATABASE postgres TO ${DB_USER};" + +# Grant all necessary privileges to the user +psql -c "ALTER USER ${DB_USER} CREATEDB;" +psql -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${DB_USER};" + +# Create the database named test_game_stats_db. +psql -c "CREATE DATABASE test_game_stats_db OWNER ${DB_USER};" +psql -c "GRANT ALL PRIVILEGES ON DATABASE test_game_stats_db TO ${DB_USER};" + +# Run Django migrations +cd /app +source venv/bin/activate +python manage.py migrate # Stop the PostgreSQL server after setting the password pg_ctl stop -D /var/lib/postgresql/data From 93df9707e2c115822e176be8cc0147cad32f6e00 Mon Sep 17 00:00:00 2001 From: abbastoof Date: Mon, 8 Jul 2024 19:46:30 +0300 Subject: [PATCH 12/15] Update GameHistory microservice: - Add GameStat model to models.py - Add GameStatSerializer to serializers.py - Add GameStatViewSet to views.py - Update URLs to include GameStat endpoints - Modify existing tests in test_game_history.py to accommodate new GameStat model - Add tests for GameStat CRUD operations in test_game_history.py --- .../game_history/game_data/models.py | 9 ++ .../game_history/game_data/serializers.py | 6 +- .../game_history/game_data/urls.py | 23 +++- .../game_history/game_data/views.py | 58 ++++++++- .../game_history/tests/test_game_history.py | 110 +++++++++++++++++- 5 files changed, 201 insertions(+), 5 deletions(-) diff --git a/Backend/game_history/game_history/game_data/models.py b/Backend/game_history/game_history/game_data/models.py index 1d9cc65..58c6796 100755 --- a/Backend/game_history/game_history/game_data/models.py +++ b/Backend/game_history/game_history/game_data/models.py @@ -10,3 +10,12 @@ class GameHistory(models.Model): end_time = models.DateTimeField(null=True, blank=True) def __str__(self): return f"Game {self.game_id}: {self.player1_id} vs {self.player2_id} - Winner: {self.winner_id}" + +class GameStat(models.Model): + game_id = models.OneToOneField(GameHistory, on_delete=models.CASCADE, primary_key=True) # this field is a foreign key to the GameHistory model + player1_score = models.IntegerField() + player2_score = models.IntegerField() + total_hits = models.IntegerField() + longest_rally = models.IntegerField() + def __str__(self): + return f"Stats for Game {self.game_id.game_id}: {self.player1_score} vs {self.player2_score} - Total Hits: {self.total_hits}, Longest Rally: {self.longest_rally}" diff --git a/Backend/game_history/game_history/game_data/serializers.py b/Backend/game_history/game_history/game_data/serializers.py index 742ab0a..f21371c 100755 --- a/Backend/game_history/game_history/game_data/serializers.py +++ b/Backend/game_history/game_history/game_data/serializers.py @@ -1,9 +1,13 @@ # game_data/serializers.py from rest_framework import serializers -from .models import GameHistory +from .models import GameHistory, GameStat class GameHistorySerializer(serializers.ModelSerializer): class Meta: model = GameHistory fields = '__all__' +class GameStatSerializer(serializers.ModelSerializer): + class Meta: + model = GameStat + fields = '__all__' diff --git a/Backend/game_history/game_history/game_data/urls.py b/Backend/game_history/game_history/game_data/urls.py index 396cdbc..d540a12 100755 --- a/Backend/game_history/game_history/game_data/urls.py +++ b/Backend/game_history/game_history/game_data/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import GameHistoryViewSet +from .views import GameHistoryViewSet, GameStatViewSet urlpatterns = [ path( @@ -23,4 +23,25 @@ ), name="game-history-detail", ), + path( + "game-stat/", + GameStatViewSet.as_view( + { + "get": "list", + "post": "create" + } + ), + name="gamestat-list", + ), + path( + "game-stat//", + GameStatViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "delete": "destroy", + } + ), + name="gamestat-detail", + ), ] diff --git a/Backend/game_history/game_history/game_data/views.py b/Backend/game_history/game_history/game_data/views.py index 3404c63..39816e7 100755 --- a/Backend/game_history/game_history/game_data/views.py +++ b/Backend/game_history/game_history/game_data/views.py @@ -1,6 +1,6 @@ from rest_framework import viewsets, status -from .models import GameHistory -from .serializers import GameHistorySerializer +from .models import GameHistory, GameStat +from .serializers import GameHistorySerializer, GameStatSerializer from rest_framework.response import Response from django.shortcuts import get_object_or_404 @@ -57,3 +57,57 @@ def destroy(self, request, pk=None, *args, **kwargs): instance = get_object_or_404(self.get_queryset(), pk=pk) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) + +class GameStatViewSet(viewsets.ModelViewSet): + """ + A viewset for viewing and editing game stat instances. + """ + serializer_class = GameStatSerializer + queryset = GameStat.objects.all() + + def create(self, request, *args, **kwargs): + """ + Create a new game stat record. + """ + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(): + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request, *args, **kwargs): + """ + List all game stat records. + """ + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request, pk=None, *args, **kwargs): + """ + Retrieve a specific game stat record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request, pk=None, *args, **kwargs): + """ + Update a specific game stat record. + """ + partial = kwargs.pop('partial', False) + instance = get_object_or_404(self.get_queryset(), pk=pk) + serializer = self.get_serializer(instance, data=request.data, partial=partial) + if serializer.is_valid(): + self.perform_update(serializer) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, pk=None, *args, **kwargs): + """ + Delete a specific game stat record. + """ + instance = get_object_or_404(self.get_queryset(), pk=pk) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/Backend/game_history/game_history/game_history/tests/test_game_history.py b/Backend/game_history/game_history/game_history/tests/test_game_history.py index f9389be..e1e67d3 100755 --- a/Backend/game_history/game_history/game_history/tests/test_game_history.py +++ b/Backend/game_history/game_history/game_history/tests/test_game_history.py @@ -2,7 +2,7 @@ from django.urls import reverse from rest_framework.test import APIClient from rest_framework import status -from game_data.models import GameHistory +from game_data.models import GameHistory, GameStat from django.utils.timezone import now from datetime import datetime @@ -103,3 +103,111 @@ def test_create_game_history_validation_error(api_client): response = api_client.post(url, data, format='json') assert response.status_code == status.HTTP_400_BAD_REQUEST assert 'player2_id' in response.data + +@pytest.mark.django_db +def test_primary_key_increment(api_client): + initial_count = GameHistory.objects.count() + + # Create a new game history entry + data = { + 'player1_id': 1, + 'player2_id': 2, + 'winner_id': 1, + 'start_time': '2024-07-03T12:00:00Z', + 'end_time': '2024-07-03T12:30:00Z' + } + response = api_client.post('/game-history/', data, format='json') + assert response.status_code == 201 + + # Check the count after insertion + new_count = GameHistory.objects.count() + assert new_count == initial_count + 1 + + # Get the latest entry and check the primary key + latest_entry = GameHistory.objects.latest('game_id') + print(latest_entry.game_id) # This will print the latest primary key value + +@pytest.mark.django_db +def test_create_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + url = reverse('gamestat-list') + data = { + 'game_id': game.game_id, + 'player1_score': 10, + 'player2_score': 5, + 'total_hits': 15, + 'longest_rally': 4 + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert GameStat.objects.count() == 1 + game_stat = GameStat.objects.first() + assert game_stat.player1_score == 10 + assert game_stat.player2_score == 5 + assert game_stat.total_hits == 15 + assert game_stat.longest_rally == 4 + +@pytest.mark.django_db +def test_list_game_stat(api_client): + game1 = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + GameStat.objects.create(game_id=game1, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + game2 = GameHistory.objects.create(player1_id=3, player2_id=4, winner_id=4, start_time=now()) + GameStat.objects.create(game_id=game2, player1_score=8, player2_score=7, total_hits=20, longest_rally=5) + + url = reverse('gamestat-list') + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 + assert response.data[0]['player1_score'] == 10 + assert response.data[0]['player2_score'] == 5 + assert response.data[0]['total_hits'] == 15 + assert response.data[0]['longest_rally'] == 4 + assert response.data[1]['player1_score'] == 8 + assert response.data[1]['player2_score'] == 7 + assert response.data[1]['total_hits'] == 20 + assert response.data[1]['longest_rally'] == 5 + +@pytest.mark.django_db +def test_retrieve_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game_stat = GameStat.objects.create(game_id=game, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + url = reverse('gamestat-detail', args=[game_stat.pk]) + response = api_client.get(url, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['player1_score'] == game_stat.player1_score + assert response.data['player2_score'] == game_stat.player2_score + assert response.data['total_hits'] == game_stat.total_hits + assert response.data['longest_rally'] == game_stat.longest_rally + +@pytest.mark.django_db +def test_update_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game_stat = GameStat.objects.create(game_id=game, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + url = reverse('gamestat-detail', args=[game_stat.pk]) + data = { + 'game_id': game.game_id, + 'player1_score': 12, + 'player2_score': 6, + 'total_hits': 18, + 'longest_rally': 5 + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + game_stat.refresh_from_db() + assert game_stat.player1_score == 12 + assert game_stat.player2_score == 6 + assert game_stat.total_hits == 18 + assert game_stat.longest_rally == 5 + +@pytest.mark.django_db +def test_delete_game_stat(api_client): + game = GameHistory.objects.create(player1_id=1, player2_id=2, winner_id=1, start_time=now()) + game_stat = GameStat.objects.create(game_id=game, player1_score=10, player2_score=5, total_hits=15, longest_rally=4) + + url = reverse('gamestat-detail', args=[game_stat.pk]) + response = api_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert GameStat.objects.count() == 0 From 910b84d462d88f43ee759a8c81603d53efe76c64 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 19:58:55 +0300 Subject: [PATCH 13/15] Update .flake8 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 209c63e..155acdf 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] exclude = venv/* -max-line-length = 79 +max-line-length = 99 From 7cc581fa73938ea9b75aa8f68c9aa7ba3436a5a5 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 20:20:34 +0300 Subject: [PATCH 14/15] Update Game History microservice README and integrate GameStat model and endpoints --- Backend/game_history/README.md | 82 ++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/Backend/game_history/README.md b/Backend/game_history/README.md index b4f2799..7402e06 100644 --- a/Backend/game_history/README.md +++ b/Backend/game_history/README.md @@ -2,7 +2,7 @@ ### Overview -The Game History microservice is a key component of the Transcendence project at 42 School. This service is responsible for recording and managing the history of ping pong games played by users. It supports creating, retrieving, updating, and deleting game history records. +The Game History microservice is a key component of the Transcendence project at 42 School. This service is responsible for recording and managing the history of ping pong games played by users. It supports creating, retrieving, updating, and deleting game history and game statistics records. ### Directory Structure @@ -57,37 +57,61 @@ The `GameHistory` model represents a record of a game played between two users. - `player2_id`: Integer, ID of the second player. - `winner_id`: Integer, ID of the winning player. - `start_time`: DateTime, the start time of the game. -- `end_time`: DateTime, the end time of the game (auto-populated). +- `end_time`: DateTime, the end time of the game (optional). + +The `GameStat` model represents the statistics of a game, linked to a `GameHistory` record. It includes the following fields: + +- `game_id`: OneToOneField, primary key linked to `GameHistory`. +- `player1_score`: Integer, score of the first player. +- `player2_score`: Integer, score of the second player. +- `total_hits`: Integer, total number of hits in the game. +- `longest_rally`: Integer, the longest rally in the game. ### Serializers -The `GameHistorySerializer` converts model instances to JSON format and validates incoming data. +- The `GameHistorySerializer` converts `GameHistory` model instances to JSON format and validates incoming data. +- The `GameStatSerializer` converts `GameStat` model instances to JSON format and validates incoming data. ### Views -The `GameHistoryViewSet` handles the CRUD operations for game history records. +- The `GameHistoryViewSet` handles the CRUD operations for game history records. +- The `GameStatViewSet` handles the CRUD operations for game statistics records. ### URLs -The microservice defines several endpoints to interact with the game history data. These endpoints are defined in the `game_data/urls.py` file. Here is an overview of how to access them: - -- **List and Create Game History Records:** - ``` - GET /game-history/ - POST /game-history/ - ``` -- **Retrieve, Update, and Delete a Specific Game History Record:** - ``` - GET /game-history// - PUT /game-history// - DELETE /game-history// - ``` +The microservice defines several endpoints to interact with the game history and game statistics data. These endpoints are defined in the `game_data/urls.py` file. Here is an overview of how to access them: + +- **Game History Endpoints:** + - **List and Create Game History Records:** + ``` + GET /game-history/ + POST /game-history/ + ``` + - **Retrieve, Update, and Delete a Specific Game History Record:** + ``` + GET /game-history// + PUT /game-history// + DELETE /game-history// + ``` + +- **Game Stat Endpoints:** + - **List and Create Game Stat Records:** + ``` + GET /game-stat/ + POST /game-stat/ + ``` + - **Retrieve, Update, and Delete a Specific Game Stat Record:** + ``` + GET /game-stat// + PUT /game-stat// + DELETE /game-stat// + ``` ### Tests #### Directory Structure -The tests for the Game History microservice are located in the `game_history/game_history/tests/` directory. The tests ensure that the CRUD operations for game history records are working correctly. +The tests for the Game History microservice are located in the `game_history/game_history/tests/` directory. The tests ensure that the CRUD operations for game history and game statistics records are working correctly. #### Test Cases @@ -103,12 +127,24 @@ The tests for the Game History microservice are located in the `game_history/gam 4. **Test Update Game History** - Verifies that a specific game history record can be updated successfully. -5. **Test Partial Update Game History** - - Verifies that a specific game history record can be partially updated successfully. - -6. **Test Delete Game History** +5. **Test Delete Game History** - Verifies that a specific game history record can be deleted successfully. +6. **Test Create Game Stat** + - Verifies that a game statistics record can be created successfully. + +7. **Test List Game Stats** + - Verifies that a list of game statistics records can be retrieved successfully. + +8. **Test Retrieve Game Stat** + - Verifies that a specific game statistics record can be retrieved successfully. + +9. **Test Update Game Stat** + - Verifies that a specific game statistics record can be updated successfully. + +10. **Test Delete Game Stat** + - Verifies that a specific game statistics record can be deleted successfully. + ### Running Tests To run the tests, use the following commands: @@ -127,4 +163,4 @@ To run the tests, use the following commands: ### Conclusion -The Game History microservice is an essential part of the Transcendence project, providing a robust solution for managing game history records. This documentation provides an overview of the setup, implementation, and testing of the service. For further details, refer to the respective source code files in the project directory. +The Game History microservice is an essential part of the Transcendence project, providing a robust solution for managing game history and game statistics records. This documentation provides an overview of the setup, implementation, and testing of the service. For further details, refer to the respective source code files in the project directory. From 5859ffb67ab522f65c20ab5baeea0faa3a50d4d6 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Mon, 8 Jul 2024 21:31:08 +0300 Subject: [PATCH 15/15] Refactor CI pipeline to use multiple Dockerfiles for linting, testing, and publishing stages --- .github/workflows/ci-pipeline.yml | 78 +++++++++++++++++++++ .github/workflows/ci.yml | 54 +++++++++++++- .github/workflows/merge-develop-to-main.yml | 29 ++++++++ .github/workflows/merge-to-develop.yml | 34 +++++++++ Dockerfile | 16 +++-- Dockerfile.lint | 25 +++++++ Dockerfile.publish | 27 +++++++ Dockerfile.test | 25 +++++++ 8 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci-pipeline.yml create mode 100644 .github/workflows/merge-develop-to-main.yml create mode 100644 .github/workflows/merge-to-develop.yml create mode 100644 Dockerfile.lint create mode 100644 Dockerfile.publish create mode 100644 Dockerfile.test diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml new file mode 100644 index 0000000..f0b4b71 --- /dev/null +++ b/.github/workflows/ci-pipeline.yml @@ -0,0 +1,78 @@ +name: CI Pipeline + +on: + push: + branches: + - feature/* + - fix/* + - refactor/* + - chore/* + - develop + +jobs: + lint-project: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Build and run linter + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.lint + + run-tests: + needs: lint-project + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Build and run tests + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.test + + publish-test-image: + needs: run-tests + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: abbastoof + password: ${{ secrets.GH_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.publish + push: true + tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }} + + merge-develop: + runs-on: ubuntu-latest + needs: publish-test-image + if: github.ref_name == 'develop' + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Fetch all branches + run: git fetch --all + + - name: Merge branches into develop + run: | + git checkout develop + git merge --no-ff ${{ github.ref_name }} + git push origin develop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97e4b3c..e7946a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,8 @@ name: CI Pipeline on: push: branches: - - feature/015-game-history-microservice + - feature/* + - develop jobs: lint-project: @@ -156,7 +157,7 @@ jobs: run: | echo "[flake8]" > .flake8 echo "exclude = venv/*" >> .flake8 - echo "max-line-length = 79" >> .flake8 + echo "max-line-length = 99" >> .flake8 - name: Verify installed packages run: | @@ -167,10 +168,38 @@ jobs: run: | . venv/bin/activate flake8 . + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - publish-test-image: + run-tests: needs: lint-project runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m venv venv + . venv/bin/activate + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: | + . venv/bin/activate + pytest --maxfail=1 --disable-warnings + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-test-image: + needs: run-tests + runs-on: ubuntu-latest steps: - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -188,5 +217,24 @@ jobs: - name: Build and push uses: docker/build-push-action@v3 with: + context: . + file: ./Dockerfile push: true tags: ghcr.io/${{ github.repository }}:feature/015-game-history-microservice + + merge-develop: + runs-on: ubuntu-latest + needs: publish-test-image + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Fetch all branches + run: git fetch --all + + - name: Merge branches into develop + run: | + git checkout develop + git merge feature/015-game-history-microservice + # Add other branches as needed + git push origin develop diff --git a/.github/workflows/merge-develop-to-main.yml b/.github/workflows/merge-develop-to-main.yml new file mode 100644 index 0000000..a0b628f --- /dev/null +++ b/.github/workflows/merge-develop-to-main.yml @@ -0,0 +1,29 @@ +name: Merge Develop to Main + +on: + pull_request: + branches: + - develop + +jobs: + merge-to-main: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Merge develop into main + run: | + git checkout main + git merge develop --no-ff -m "Merging develop into main" + git push origin main diff --git a/.github/workflows/merge-to-develop.yml b/.github/workflows/merge-to-develop.yml new file mode 100644 index 0000000..d0bc101 --- /dev/null +++ b/.github/workflows/merge-to-develop.yml @@ -0,0 +1,34 @@ +name: Merge to Develop + +on: + push: + branches: + - 'feature/*' + - 'fix/*' + - 'refactor/*' + - 'chore/*' + +jobs: + merge-to-develop: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch all branches + run: git fetch --all + + - name: Merge branches into develop + run: | + BRANCHES=$(git for-each-ref --format '%(refname:short)' refs/heads/feature refs/heads/fix refs/heads/refactor refs/heads/chore) + for branch in $BRANCHES; do + git checkout develop + git merge $branch --no-ff -m "Merging $branch into develop" + done + git push origin develop diff --git a/Dockerfile b/Dockerfile index d635401..7021f85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,27 @@ -FROM python:3.11 +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 -COPY ./requirements.txt requirements.txt +COPY ./requirements.txt /requirements.txt RUN python -m pip install --upgrade pip RUN pip install setuptools==58.0.4 wheel -RUN pip --timeout=1000 install -r requirements.txt +RUN pip --timeout=1000 install -r /requirements.txt WORKDIR /app COPY . /app -RUN chown -R www-data:www-data /app +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app USER www-data EXPOSE 8000 -CMD ["manage.py", "runserver", "0.0.0.0:8000"] +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/Dockerfile.lint b/Dockerfile.lint new file mode 100644 index 0000000..ee26f45 --- /dev/null +++ b/Dockerfile.lint @@ -0,0 +1,25 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip install flake8 + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +CMD ["flake8", "."] diff --git a/Dockerfile.publish b/Dockerfile.publish new file mode 100644 index 0000000..7bc9f01 --- /dev/null +++ b/Dockerfile.publish @@ -0,0 +1,27 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip install -r /requirements.txt + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..68ecf80 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,25 @@ +FROM alpine:3.20 + +# Install Python and pip +RUN apk add --no-cache python3 py3-pip && \ + python3 -m ensurepip && \ + ln -sf python3 /usr/bin/python + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY ./requirements.txt /requirements.txt +RUN python -m pip install --upgrade pip +RUN pip install setuptools==58.0.4 wheel +RUN pip install -r /requirements.txt + +WORKDIR /app + +COPY . /app + +RUN addgroup -S www-data && adduser -S www-data -G www-data && \ + chown -R www-data:www-data /app + +USER www-data + +CMD ["pytest", "--maxfail=1", "--disable-warnings"]