diff --git a/django/api/__init__.py b/django/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/api/apps.py b/django/api/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..f1d64a8f188de61f39d2b5c7d91427c0c27df249 --- /dev/null +++ b/django/api/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/django/api/urls.py b/django/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..592c29ff3ef136da77d206cf9521c9bcad8da958 --- /dev/null +++ b/django/api/urls.py @@ -0,0 +1,13 @@ +""" + File for setting the URLs used from the api django app. +""" + +from django.urls import path +from . import views + + +urlpatterns = [ + path('box/search', views.ButtonBoxSearchView.as_view(), name='box_search'), + path('box/use', views.ButtonBoxUseView.as_view(), name='box_use'), + path('box/use/count', views.ButtonBoxUseCountView.as_view(), name='box_use_count'), +] diff --git a/django/api/views.py b/django/api/views.py new file mode 100644 index 0000000000000000000000000000000000000000..aaeea0da7719c25b6bab7d012322b88673d8584d --- /dev/null +++ b/django/api/views.py @@ -0,0 +1,442 @@ +""" + File for setting the view classes used from the api django app. +""" + +from datetime import datetime, timezone + +from shared_models.models import ButtonBox, ButtonBoxUse +from shared_models.serializers import ButtonBoxSerializer, ButtonBoxUseSerializer + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.renderers import JSONRenderer +from rest_framework_simplejwt.authentication import JWTAuthentication + + +### +### HELPERS +### +def helper_get_from_parsed(parsed_name: str, parsed_value: str, parsed_type: str): + """Helper method used from the ButtonBoxUseView class for setting the return values from different 'parsed_name' and 'parsed_type'. + It will fetch or update the values from the ButtonBoxUse model. + + Parameters + ---------- + parsed_name: str + String with the name parsed. + parsed_value: str + String with the value parsed. + parsed_type: str + String with the type parsed. + + Returns + ------- + ret_dict: dict + The Dictionary with the output from executing this helper method. + ret_status: rest_framework.status + The HTTP status from executing this helper method. + """ + + founded_box = None + ret_dict = None + ret_status = status.HTTP_500_INTERNAL_SERVER_ERROR + try: + if parsed_name == "id": + founded_box = ButtonBox.objects.get(id=parsed_value) + + elif parsed_name == "name": + founded_box = ButtonBox.objects.get(name=parsed_value) + + elif parsed_name == "pin_button": + founded_box = ButtonBox.objects.get(pin_button=parsed_value) + + elif parsed_name == "pin_led": + founded_box = ButtonBox.objects.get(pin_led=parsed_value) + + else: + ret_dict = { + 'error': True, + 'error_msg': f"ERROR: unknown parameter parsed: {parsed_name}." + } + ret_status = status.HTTP_406_NOT_ACCEPTABLE + + except ButtonBox.DoesNotExist: + ret_dict = { + 'error': True, + 'error_msg': f"No Box could be founded with the '{parsed_name}': {parsed_value}" + } + ret_status = status.HTTP_404_NOT_FOUND + + except Exception as e: + ret_dict = { + 'error': True, + 'error_msg': f"ERROR happend when trying to search for the box with {parsed_name}: {parsed_value} | Error: {e}" + } + ret_status = status.HTTP_406_NOT_ACCEPTABLE + + ### If error ocurred, finish + if ret_dict is not None: + pass + + else: + if parsed_type == "add": + try: + ### Add pushed from pin + added = ButtonBoxUse(box_used=founded_box) + added.save() + + ret_dict = { + 'error': False, + 'message': "Added button press", + "button-used": { + "id": added.id, + "box_id": added.box_used.id, + 'box_name': added.box_used.name, + 'box_pin_button': added.box_used.pin_button, + 'box_pin_led': added.box_used.pin_led, + 'date': str(added.date) + } + } + ret_status = status.HTTP_200_OK + + + except Exception as e: + ret_dict = { + 'error': True, + 'message': f"Error saving the press button with output: {e}", + } + ret_status = status.HTTP_418_IM_A_TEAPOT + + elif parsed_type == "list": + try: + ### Get list pushed button + box_uses = ButtonBoxUse.objects.filter(box_used=founded_box) + serializer = ButtonBoxUseSerializer(box_uses, many=True) + ret_dict = serializer.data + ret_status = status.HTTP_200_OK + + except Exception as e: + ret_dict = { + 'error': True, + 'error_msg': f"ERROR happend when trying to search for the box with {parsed_name}: {parsed_value} | Error: {e}" + } + ret_status = status.HTTP_406_NOT_ACCEPTABLE + + else: + ret_dict = { + 'error': True, + 'error_msg': "ERROR parsing response from 'helper_get_from_parsed' function" + } + ret_status = status.HTTP_501_NOT_IMPLEMENTED + + return ret_dict, ret_status + + +### +### VIEW CLASSES +### +class ButtonBoxSearchView(APIView): + """Class for setting the API views for Searching at the ButtonBox model. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + # Get parsed + box_id = request.query_params.get("id") + box_name = request.query_params.get("name") + box_pin_button = request.query_params.get("pin_button") + box_pin_led = request.query_params.get("pin_led") + + if box_id is not None and box_name is None and box_pin_button is None and box_pin_led is None: + box_founded = ButtonBox.objects.filter(id__icontains=box_id) + + elif box_id is None and box_name is not None and box_pin_button is None and box_pin_led is None: + box_founded = ButtonBox.objects.filter(name__icontains=box_name) + + elif box_id is None and box_name is None and box_pin_button is not None and box_pin_led is None: + box_founded = ButtonBox.objects.filter(pin_button__icontains=box_pin_button) + + elif box_id is None and box_name is None and box_pin_button is None and box_pin_led is not None: + box_founded = ButtonBox.objects.filter(pin_led__icontains=box_pin_led) + + # If not, return all + else: + box_founded = ButtonBox.objects.all() + + serializer = ButtonBoxSerializer(box_founded, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class ButtonBoxUseView(APIView): + """Class for setting the API views for Searching and Adding values at the ButtonBoxUse model. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + # Get parsed + box_id = request.query_params.get("id") + box_name = request.query_params.get("name") + box_pin_button = request.query_params.get("pin_button") + box_pin_led = request.query_params.get("pin_led") + + if box_id is not None and box_name is None and box_pin_button is None and box_pin_led is None: + founded_box = ButtonBox.objects.get(id=box_id) + + elif box_id is None and box_name is not None and box_pin_button is None and box_pin_led is None: + founded_box = ButtonBox.objects.get(name=box_name) + + elif box_id is None and box_name is None and box_pin_button is not None and box_pin_led is None: + founded_box = ButtonBox.objects.get(pin_button=box_pin_button) + + elif box_id is None and box_name is None and box_pin_button is None and box_pin_led is not None: + founded_box = ButtonBox.objects.get(pin_led=box_pin_led) + + else: + founded_box = None + + # Get from parsed, if None get all + if founded_box is None: + boxes_uses = ButtonBoxUse.objects.all() + serializer = ButtonBoxUseSerializer(boxes_uses, many=True) + + else: + box_uses = ButtonBoxUse.objects.filter(box_used=founded_box) + serializer = ButtonBoxUseSerializer(box_uses, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, format=None): + """Method for dealing with the POST requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the POST request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + # Get the box name from the query parameters (if Nonne, default to empty str) + parsed_id = request.data.get("id") + parsed_name = request.data.get("name") + # parsed_pin_number = request.data.get("pin_number") + parsed_pin_button = request.data.get("pin_button") + parsed_pin_led = request.data.get("pin_led") + + if parsed_id is None and parsed_name is None and parsed_pin_button is None and parsed_pin_led is None: + ret_dict = { + "error": True, + "message": "Unable to add box used from this end-point withtout parsed info from 'name', 'pin_button' or 'pin_led'" + } + ret_status = status.HTTP_400_BAD_REQUEST + + elif parsed_id is not None and parsed_name is not None and parsed_pin_button is not None and parsed_pin_led is not None: + ret_dict = { + "error": True, + "message": "Unable to add box used from this end-point with MULTIPLE parsed infos from 'id', 'name', 'pin_button' and 'pin_led'. Use only one" + } + ret_status = status.HTTP_400_BAD_REQUEST + + ### ID + elif parsed_id is not None and parsed_name is None and parsed_pin_button is None and parsed_pin_led is None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='id', + parsed_value=parsed_id, + parsed_type='add' + ) + + ### NAME + elif parsed_name is not None and parsed_pin_button is None and parsed_pin_led is None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='name', + parsed_value=parsed_name, + parsed_type='add' + ) + + ### PIN_BUTTON + elif parsed_name is None and parsed_pin_button is not None and parsed_pin_led is None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='pin_button', + parsed_value=parsed_pin_button, + parsed_type='add' + ) + + ### PIN_LED + elif parsed_name is None and parsed_pin_button is None and parsed_pin_led is not None: + ret_dict, ret_status = helper_get_from_parsed( + parsed_name='pin_led', + parsed_value=parsed_pin_led, + parsed_type='add' + ) + + else: + ret_dict = { + "error": True, + "message": "Option not implemented..." + } + ret_status = status.HTTP_501_NOT_IMPLEMENTED + + return Response(ret_dict, status=ret_status) + + +class ButtonBoxUseCountView(APIView): + """Class for setting the API views for getting the amount of uses from a Button Box. + It is mostly used from Grafana for getting the fancy requests from date windows. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + ### + ### EXAMPLE of the grafana used URL ;) + ### + ### http://localhost:8000/api/box/use/count?from=${__from}&to=${__to} + ### + + ### GET PARSED + query_param_from = request.query_params.get("from") + query_param_to = request.query_params.get("to") + + if query_param_from is not None and query_param_to is None: + ret_list = [ + { + 'error': True, + 'error_msg': "Query parameter for 'from' is parsed and 'to' is not parsed. You have to use BOUTH or NONE" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + elif query_param_from is None and query_param_to is not None: + ret_list = [ + { + 'error': True, + 'error_msg': "Query parameter for 'to' is parsed and 'from' is not parsed. You have to use BOUTH or NONE" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + ### RETURN ALL + elif query_param_from is None and query_param_to is None: + + ret_list = [] + for button_box in ButtonBox.objects.all(): + ret_list.append( + { + "name": button_box.name, + "count": ButtonBoxUse.objects.filter(box_used=button_box).count() + } + ) + + ret_status = status.HTTP_200_OK + + ### RETURN FROM TIME FRAME + elif query_param_from is not None and query_param_to is not None: + + try: + date_from = datetime.fromtimestamp(int(query_param_from) / 1000, timezone.utc) + + except Exception as e: + ret_list = [ + { + 'error': True, + 'error_msg': f"Error parsing epoch time to datetime from parsed 'date_from' parameter with error: {e}" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + try: + date_to = datetime.fromtimestamp(int(query_param_to) / 1000, timezone.utc) + + except Exception as e: + ret_list = [ + { + 'error': True, + 'error_msg': f"Error parsing epoch time to datetime from parsed 'date_to' parameter with error: {e}" + } + ] + ret_status = status.HTTP_400_BAD_REQUEST + + ret_list = [] + for button_box in ButtonBox.objects.all(): + button_name = button_box.name + count = 0 + for button_use in ButtonBoxUse.objects.filter(date__gt=date_from, date__lt=date_to): + if button_name == button_use.box_name: + count += 1 + ret_list.append( + { + "name": button_name, + "count": count, + "date": button_use.date + } + ) + ret_status = status.HTTP_200_OK + + else: + ret_list = [ + { + 'error': True, + 'error_msg': "Not implemented" + } + ] + ret_status = status.HTTP_501_NOT_IMPLEMENTED + + return Response(ret_list, status=ret_status) diff --git a/django/api_admin/__init__.py b/django/api_admin/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/api_admin/apps.py b/django/api_admin/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..bc61874d842807701326a0787bfe612b44c2d611 --- /dev/null +++ b/django/api_admin/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class ApiAdminConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "api_admin" diff --git a/django/api_admin/urls.py b/django/api_admin/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..6301cd4dd26e485cece9e9612421c1c76856555b --- /dev/null +++ b/django/api_admin/urls.py @@ -0,0 +1,13 @@ +""" + File for setting the URLs used from the api_admin django app. +""" + +from django.urls import path +from . import views + + +urlpatterns = [ + path('box', views.AdminButtonBoxListCreateDeleteView.as_view(), name='admin_box'), + path('box/edit/<int:pk>', views.AdminButtonBoxRetrieveUpdateDestroyView.as_view(), name='admin_box_edit'), + path('box/use', views.AdminButtonBoxUseListCreateView.as_view(), name='admin_box_use'), +] diff --git a/django/api_admin/views.py b/django/api_admin/views.py new file mode 100644 index 0000000000000000000000000000000000000000..cc8d154045a4c7fafe3d493932e0243733ad149f --- /dev/null +++ b/django/api_admin/views.py @@ -0,0 +1,76 @@ +""" + File for setting the view classes used from the api_admin django app. +""" + + +from shared_models.models import ButtonBox, ButtonBoxUse +from shared_models.serializers import ButtonBoxSerializer, ButtonBoxUseSerializer +from rest_framework import generics, status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from backend.settings import DEFAULT_DJANGO_DEBUG + + +class AdminButtonBoxListCreateDeleteView(generics.ListCreateAPIView): + """Class for setting the API views for Create, Read and Delete the entries at the ButtonBox model. + """ + + permission_classes = [IsAuthenticated] + + queryset = ButtonBox.objects.all() + serializer_class = ButtonBoxSerializer + + def delete(self, request, *args, **kwargs): + """Method for deleting all ButtonBox model objects. + """ + + # Delete only by debug mode + if DEFAULT_DJANGO_DEBUG: + ButtonBox.objects.all().delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + else: + ret_dict = { + "error": False, + "message": "You can only delete all in debub mode --> set the 'DEFAULT_DJANGO_DEBUG' to 'True'" + } + return Response(ret_dict, status=status.HTTP_412_PRECONDITION_FAILED) + + +class AdminButtonBoxRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): + """Class for setting the API views for Updating entries at the ButtonBox model. + """ + + permission_classes = [IsAuthenticated] + + queryset = ButtonBox.objects.all() + serializer_class = ButtonBoxSerializer + ### pk --> primary_key + lookup_field = "pk" + + +class AdminButtonBoxUseListCreateView(generics.ListCreateAPIView): + """Class for setting the API views for Creating entries at the ButtonBoxUse model. + """ + + permission_classes = [IsAuthenticated] + + queryset = ButtonBoxUse.objects.all() + serializer_class = ButtonBoxUseSerializer + + def delete(self, request, *args, **kwargs): + """Method for deleting all ButtonBoxUse model objects. + """ + + # Delete only by debug mode + if DEFAULT_DJANGO_DEBUG: + ButtonBoxUse.objects.all().delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + else: + ret_dict = { + "error": False, + "message": "You can only delete all in debub mode --> set the 'DEFAULT_DJANGO_DEBUG' to 'True'" + } + return Response(ret_dict, status=status.HTTP_412_PRECONDITION_FAILED) diff --git a/django/api_auth/__init__.py b/django/api_auth/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/api_auth/apps.py b/django/api_auth/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..0d4e91be1a4ff0c2e01c06b07713971624c9c76e --- /dev/null +++ b/django/api_auth/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class ApiAuthConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "api_auth" diff --git a/django/api_auth/templates/login.html b/django/api_auth/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..f7980935c847dcdf75a8e0fca18804d6585c8248 --- /dev/null +++ b/django/api_auth/templates/login.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <title>Login</title> +</head> + +<body> + <h2>Login</h2> + {% if user.is_authenticated %} + <p>Logged in as {{ user.username }}</p> + <a href="{% url 'logout' %}">Logout</a> + {% else %} + <p>You are not logged in.</p> + {% endif %} + <form method="post"> + {% csrf_token %} + {{ form.as_p }} + <button type="submit">Login</button> + </form> +</body> + +</html> \ No newline at end of file diff --git a/django/api_auth/templates/logout.html b/django/api_auth/templates/logout.html new file mode 100644 index 0000000000000000000000000000000000000000..71a9b56cb2007d0b2b39b2a95e1fa16aac9c065c --- /dev/null +++ b/django/api_auth/templates/logout.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <title>Logout</title> +</head> + +<body> + <h2>You have been logged out.</h2> + <a href="{% url 'login' %}">Log in again</a> +</body> + +</html> \ No newline at end of file diff --git a/django/api_auth/urls.py b/django/api_auth/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a7872fb30fd2fc82ba1e9cb1ab155f3edddafe44 --- /dev/null +++ b/django/api_auth/urls.py @@ -0,0 +1,22 @@ +""" + File for setting the URLs used from the api_auth django app. +""" + +### TOKEN +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + +### LOGIN +from django.contrib.auth import views as auth_views + +from django.urls import path +from . import views + + +urlpatterns = [ + path('test/open', views.TestOpenView.as_view(), name='test-open'), + path('test/closed', views.TestClosedView.as_view(), name='test-closed'), + path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'), + path('logout/', views.custom_logout, name='logout'), + path("token/", TokenObtainPairView.as_view(), name='token_obtain_pair'), + path("token/refresh/", TokenRefreshView.as_view(), name='token_refresh'), +] diff --git a/django/api_auth/views.py b/django/api_auth/views.py new file mode 100644 index 0000000000000000000000000000000000000000..1946c4f29282833c8d41bae9dc554abcca95a51a --- /dev/null +++ b/django/api_auth/views.py @@ -0,0 +1,97 @@ + +""" +TODO: Add this !!! +""" +from rest_framework import status +from rest_framework.views import APIView +from rest_framework import permissions +# from rest_framework.permissions import IsAuthenticated +from rest_framework.renderers import JSONRenderer +from rest_framework_simplejwt.authentication import JWTAuthentication +from django.views.decorators.http import require_GET +from django.contrib.auth import logout +from django.shortcuts import redirect +from django.http import JsonResponse + + +class TestOpenView(APIView): + """Class for testing an end-point without authentication. + """ + + permission_classes = [permissions.AllowAny] + + def get(self, request=None, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + ret_dict = { + "error": False, + "message": "This is the auth test open end-point" + } + + return JsonResponse(ret_dict, status=status.HTTP_200_OK) + + +class TestClosedView(APIView): + """Class for testing the authentication. + """ + + # permission_classes = [IsAuthenticated] + authentication_classes = [JWTAuthentication] + renderer_classes = [JSONRenderer] + + def get(self, request=None, format=None): + """Method for dealing with the GET requests done at this view. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + format: None + If some format was sent. + + Returns + ------- + rest_framework.response.Response + The response object. + """ + + ret_dict = { + "error": False, + "message": "You are authenticated" + } + + return JsonResponse(ret_dict, status=status.HTTP_200_OK) + + +# from rest_framework.response import Response +@require_GET +def custom_logout(request): + """Method for logging out from django. + + Parameters + ---------- + request: rest_framework.request.Request + The parameters from the GET request. + + Returns + ------- + redirect: + Redirects to the login page. + """ + + logout(request) + # return HttpResponse("You have been logged out.") # Temporary for debugging + return redirect('/auth/login/') diff --git a/django/backend/__init__.py b/django/backend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/backend/asgi.py b/django/backend/asgi.py new file mode 100644 index 0000000000000000000000000000000000000000..20e4b4470f67aecfe8eb8d6c6bcb481761221224 --- /dev/null +++ b/django/backend/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for backend 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.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") + +application = get_asgi_application() diff --git a/django/backend/settings.py b/django/backend/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..146db36841427d710d59a49bd36776c386a2a982 --- /dev/null +++ b/django/backend/settings.py @@ -0,0 +1,212 @@ +""" +Django settings for myproject project. + +Generated by 'django-admin startproject' using Django 5.1.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" +import os +from pathlib import Path +from datetime import timedelta +from dotenv import load_dotenv + + +# 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.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-9^f+%n36vxg-^q-iu1$x4&+--&u+i4ds@!xhe@+i8w(-wg00bu" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] + +### +### LOAD VARIABLES +### + +# Try loading from the .env file, if it exists... +load_dotenv() + +try: + DEFAULT_TOKEN_EXPIRE_MIN = int(os.getenv("DEFAULT_TOKEN_EXPIRE_MIN")) + +except Exception: + DEFAULT_TOKEN_EXPIRE_MIN = 10080 + +try: + DEFAULT_TOKEN_REFRESH_DAYS = int(os.getenv("DEFAULT_TOKEN_REFRESH_DAYS")) + +except Exception: + DEFAULT_TOKEN_REFRESH_DAYS = 7 + +try: + fetched = int(os.getenv("DEFAULT_DJANGO_DEBUG")) + if fetched in ('YES', 'yes', 'true', 'True', 'TRUE'): + DEFAULT_DJANGO_DEBUG = True + else: + DEFAULT_DJANGO_DEBUG = False + +except Exception: + DEFAULT_DJANGO_DEBUG = False + +print("\n---> DJANGO VARIABLES") +print(f"DEFAULT_TOKEN_EXPIRE_MIN: {DEFAULT_TOKEN_EXPIRE_MIN}") +print(f"DEFAULT_TOKEN_REFRESH_DAYS: {DEFAULT_TOKEN_REFRESH_DAYS}") +print(f"DEFAULT_DJANGO_DEBUG: {DEFAULT_DJANGO_DEBUG}") +print("<---DJANGO VARIABLES\n") + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + ### ADDED + "rest_framework", + ### SWAGGER + "drf_yasg", + ### JWT TOKEN + "rest_framework_simplejwt", + ### SSL + "sslserver", + ### MY STUFF + "shared_models", + "api", + "api_auth", + "api_admin", +] + +### +### AUTH +### +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', # Require authentication for all endpoints + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ), +} +SESSION_ENGINE = "django.contrib.sessions.backends.db" +LOGIN_REDIRECT_URL = '/swagger/' # Redirect here after login +LOGIN_URL = '/auth/login/' # The URL to redirect to for login +LOGOUT_REDIRECT_URL = '/auth/login/' # Redirect here after logout +### +### TOKEN +### +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=DEFAULT_TOKEN_EXPIRE_MIN), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=DEFAULT_TOKEN_REFRESH_DAYS), + 'SLIDING_TOKEN_LIFETIME': timedelta(days=30), + 'SLIDING_TOKEN_REFRESH_LIFETIME_LATE_USER': timedelta(days=3), + 'SLIDING_TOKEN_LIFETIME_LATE_USER': timedelta(days=30), +} + + +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", + ### + ### ADDED + ### + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = "backend.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 = "backend.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/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.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +# TIME_ZONE = "UTC" +TIME_ZONE = 'Europe/Berlin' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/django/backend/urls.py b/django/backend/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..4b69704129762eafac55f79634a742456f7cbc58 --- /dev/null +++ b/django/backend/urls.py @@ -0,0 +1,55 @@ +""" +URL configuration for backend project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/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 rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from django.contrib import admin +from django.urls import path, include + + +schema_view = get_schema_view( + openapi.Info( + title="c3buttons", + default_version='1.0.0', + description="Swagger Documentation for c3buttons", + # terms_of_service="https://link.to.something", + contact=openapi.Contact(name="fejao", email="dont@bug.me"), + license=openapi.License(name="MIT"), + ), + ### AUTH + public=False, + permission_classes=(permissions.IsAuthenticated,), + +) + + +urlpatterns = [ + path("admin/", admin.site.urls), + ### + ### SWAGGER + ### + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('swagger.json/', schema_view.without_ui(cache_timeout=0), name='schema-json'), + ### + ### APPS + ### + path('auth/', include('api_auth.urls')), + # path("api/box/", include("api.urls")), + path("api/", include("api.urls")), + path("api-admin/", include("api_admin.urls")), +] diff --git a/django/backend/wsgi.py b/django/backend/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..ae9503c61e3d7dadd1a1a77229acfb6c9f4db746 --- /dev/null +++ b/django/backend/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for backend 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.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") + +application = get_wsgi_application() diff --git a/django/manage.py b/django/manage.py new file mode 100755 index 0000000000000000000000000000000000000000..1917e46e5a9417d34d1131e11fcba9665feb978e --- /dev/null +++ b/django/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", "backend.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/django/requirements.txt b/django/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..54dae6864951a8f32dedb9cc53afe27f68471fbc --- /dev/null +++ b/django/requirements.txt @@ -0,0 +1,10 @@ +dotenv +### DJANGO +django +djangorestframework +### SWAGGER +drf-yasg +### TOKEN +djangorestframework-simplejwt +### SSL +django-sslserver diff --git a/django/shared_models/__init__.py b/django/shared_models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/shared_models/apps.py b/django/shared_models/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..6668dfbfcd134da33dfefcb3ff53268296f68fdd --- /dev/null +++ b/django/shared_models/apps.py @@ -0,0 +1,12 @@ +""" + Default setted file from django for an added app. +""" + +from django.apps import AppConfig + + +class SharedModelsConfig(AppConfig): + """Default setted SharedModelsConfig class for the app. + """ + default_auto_field = "django.db.models.BigAutoField" + name = "shared_models" diff --git a/django/shared_models/migrations/0001_initial.py b/django/shared_models/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..57ae4cea77ddbdda0ea267aee43b421b1541d0f7 --- /dev/null +++ b/django/shared_models/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.6 on 2025-03-03 18:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ButtonBox', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40, unique=True)), + ('pin_button', models.PositiveIntegerField(unique=True)), + ('pin_led', models.PositiveIntegerField(unique=True)), + ], + options={ + 'db_table': 'button_box', + }, + ), + migrations.CreateModel( + name='ButtonBoxUse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True)), + ('box_used', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shared_models.buttonbox')), + ], + options={ + 'db_table': 'button_box_use', + }, + ), + ] diff --git a/django/shared_models/migrations/__init__.py b/django/shared_models/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/shared_models/models.py b/django/shared_models/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f7010ef221c05783e949bf0b69139d7dd88f210c --- /dev/null +++ b/django/shared_models/models.py @@ -0,0 +1,49 @@ +""" + Serializers for the models shared among this django. +""" + +from django.db import models + + +class ButtonBox(models.Model): + """The model for storing the information about the button box. + """ + name = models.CharField(max_length=40, unique=True) + # pin_number = models.PositiveIntegerField(unique=True) + pin_button = models.PositiveIntegerField(unique=True) + pin_led = models.PositiveIntegerField(unique=True) + + class Meta: + """ Meta class for the Button Box model. + Used for setting the name ot the table at the DB. + """ + db_table = "button_box" + + def __str__(self): + """Override from the default __str__ method for returning the name of the box instead of it's ID. + """ + return str(self.name) + + +class ButtonBoxUse(models.Model): + """The model for storing the information about the button box uses. + """ + + box_used = models.ForeignKey( + ButtonBox, + on_delete=models.CASCADE, + ) + date = models.DateTimeField(auto_now_add=True, blank=True) + + class Meta: + """ Meta class for the Button Box Use model. + Used for setting the name ot the table at the DB. + """ + db_table = "button_box_use" + + @property + def box_name(self): + """Property method for fetching the name of the box for the box used. + """ + box_obj = ButtonBox.objects.get(name=self.box_used) + return box_obj.name diff --git a/django/shared_models/serializers.py b/django/shared_models/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..4bc1c3b968538ac05761f1d80fca63a5a168aa53 --- /dev/null +++ b/django/shared_models/serializers.py @@ -0,0 +1,41 @@ +""" + Serializer used for the used shared models among the apps. +""" + +from rest_framework import serializers +from .models import ButtonBox, ButtonBoxUse + + +class ButtonBoxSerializer(serializers.ModelSerializer): + """The serializer for the information about the button box. + """ + + class Meta: + """ Meta class for the ButtonBoxSerializer. + Used for returning only the information needed for the ButtonBox model. + """ + model = ButtonBox + fields = [ + # "id", + "name", + # "pin_number", + "pin_button", + "pin_led", + ] + + +class ButtonBoxUseSerializer(serializers.ModelSerializer): + """The serializer for the information about the button box uses. + """ + + class Meta: + """ Meta class for the ButtonBoxSerializer. + Used for returning only the information needed for the ButtonBoxUse model. + """ + model = ButtonBoxUse + fields = [ + # "id", + "box_used", + "box_name", + "date", + ]