From 8a95164970c5be77e765824177db0c51108edab0 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Sun, 29 Oct 2023 21:22:13 +0000 Subject: [PATCH 01/11] Create Environment entity and signal handler to setup default environs --- backend/app/__init__.py | 1 + backend/app/apps.py | 3 +++ backend/app/models/__init__.py | 1 + backend/app/models/environment.py | 24 ++++++++++++++++++++++++ backend/app/signals.py | 11 +++++++++++ 5 files changed, 40 insertions(+) create mode 100644 backend/app/models/environment.py create mode 100644 backend/app/signals.py diff --git a/backend/app/__init__.py b/backend/app/__init__.py index e69de29..583cbcb 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +default_app_config = 'app.apps.AppConfig' \ No newline at end of file diff --git a/backend/app/apps.py b/backend/app/apps.py index ed327d2..9557717 100644 --- a/backend/app/apps.py +++ b/backend/app/apps.py @@ -4,3 +4,6 @@ class AppConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'app' + + def ready(self): + import app.signals diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 4705c4f..8604388 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -13,3 +13,4 @@ from .user import User from .project import Project, ProjectMembership from .selector import Selector +from .environment import Environment diff --git a/backend/app/models/environment.py b/backend/app/models/environment.py new file mode 100644 index 0000000..d114032 --- /dev/null +++ b/backend/app/models/environment.py @@ -0,0 +1,24 @@ +from django.db import models + +import uuid + + +class Environment(models.Model): + name = models.CharField(max_length=100) + identifier = models.CharField(max_length=255, default=uuid.uuid4) + project = models.ForeignKey( + "Project", on_delete=models.CASCADE, related_name="environments" + ) + is_default = models.BooleanField(default=False) + + class Meta: + unique_together = ["identifier", "project"] + + def save(self, *args, **kwargs): + if not self.identifier: + # If identifier is not provided, assign a GUID. + self.identifier = str(uuid.uuid4()) + super(Environment, self).save(*args, **kwargs) + + def __str__(self): + return self.name diff --git a/backend/app/signals.py b/backend/app/signals.py new file mode 100644 index 0000000..7840bcf --- /dev/null +++ b/backend/app/signals.py @@ -0,0 +1,11 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models.environment import Environment +from .models.project import Project + + +@receiver(post_save, sender=Project) +def create_default_environments(sender, instance, created, **kwargs): + if created: + Environment.objects.create(name="Development", identifier="development", project=instance, is_default=True) + Environment.objects.create(name="Production", identifier="production", project=instance, is_default=True) \ No newline at end of file From 09c9ec5cd21dec7938439a478fbc1b27fc2f3852 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Sun, 29 Oct 2023 21:25:05 +0000 Subject: [PATCH 02/11] Add migration file --- backend/app/migrations/0007_environment.py | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 backend/app/migrations/0007_environment.py diff --git a/backend/app/migrations/0007_environment.py b/backend/app/migrations/0007_environment.py new file mode 100644 index 0000000..d98cc66 --- /dev/null +++ b/backend/app/migrations/0007_environment.py @@ -0,0 +1,42 @@ +# Generated by Django 4.1.5 on 2023-10-29 21:24 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("app", "0006_alter_project_api_key"), + ] + + operations = [ + migrations.CreateModel( + name="Environment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("identifier", models.CharField(default=uuid.uuid4, max_length=255)), + ("is_default", models.BooleanField(default=False)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="environments", + to="app.project", + ), + ), + ], + options={ + "unique_together": {("identifier", "project")}, + }, + ), + ] From 017dedb60f81885dfe12a83652f4b8da04e29177 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 09:49:08 +0000 Subject: [PATCH 03/11] Add tests for Environment model --- backend/.env | 1 + backend/app/tests/__init__.py | 4 ++ backend/app/tests/test_environments.py | 68 ++++++++++++++++++++++++++ backend/backend/settings.py | 27 +++++----- 4 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 backend/app/tests/test_environments.py diff --git a/backend/.env b/backend/.env index b917f06..2d63c5b 100644 --- a/backend/.env +++ b/backend/.env @@ -1,6 +1,7 @@ DEBUG=True DEVELOPMENT_MODE=True DJANGO_ALLOWED_HOSTS='localhost,0.0.0.0,127.0.0.1,192.168.1.220' +DJANGO_SETTINGS_MODULE=backend.settings DB_NAME = crumpet_db DB_USERNAME = head_baker diff --git a/backend/app/tests/__init__.py b/backend/app/tests/__init__.py index 9c4d0ae..d813816 100644 --- a/backend/app/tests/__init__.py +++ b/backend/app/tests/__init__.py @@ -1,4 +1,8 @@ +import django +django.setup() + from .test_openapi import * from .test_user_auth import * from .test_views import * from .test_projects import * +from .test_environments import * diff --git a/backend/app/tests/test_environments.py b/backend/app/tests/test_environments.py new file mode 100644 index 0000000..c9c0b1c --- /dev/null +++ b/backend/app/tests/test_environments.py @@ -0,0 +1,68 @@ +import uuid +from django.test import TestCase +from django.contrib.auth import get_user_model +from app.models import Environment, Project, ProjectMembership + +User = get_user_model() + + +class EnvironmentModelTestCase(TestCase): + def setUp(self): + self.project = Project.objects.create(name="Test Project") + self.environment = Environment.objects.create( + name="Staging", project=self.project + ) + + def test_environment_creation(self): + """ + Test if the environment is created with provided name and project. + """ + self.assertEqual(self.environment.name, "Staging") + self.assertEqual(self.environment.project, self.project) + + def test_environment_identifier(self): + """ + Test if the environment gets a UUID identifier if not provided. + """ + self.assertIsNotNone(self.environment.identifier) + self.assertTrue(isinstance(self.environment.identifier, uuid.UUID)) + + def test_unique_together_constraint(self): + """ + Test the unique together constraint for identifier and project. + """ + with self.assertRaises(Exception): + Environment.objects.create( + name="Duplicate", + identifier=self.environment.identifier, + project=self.project, + ) + + +class ProjectEnvironmentCreationTestCase(TestCase): + def setUp(self): + self.project = Project.objects.create(name="Another Test Project") + + def test_default_environments_created(self): + """ + Test if Development and Production environments are created for new projects. + """ + dev_env = Environment.objects.filter( + name="Development", project=self.project + ).first() + prod_env = Environment.objects.filter( + name="Production", project=self.project + ).first() + + self.assertIsNotNone(dev_env, "Development environment was not created.") + self.assertIsNotNone(prod_env, "Production environment was not created.") + + def test_default_environments_identifiers(self): + """ + Test if Development and Production environments have correct identifiers. + """ + dev_env = Environment.objects.get(name="Development", project=self.project) + prod_env = Environment.objects.get(name="Production", project=self.project) + + self.assertEqual(dev_env.identifier, "development") + self.assertEqual(prod_env.identifier, "production") diff --git a/backend/backend/settings.py b/backend/backend/settings.py index da6a6a7..2b74bd4 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -84,20 +84,25 @@ WSGI_APPLICATION = "backend.wsgi.application" - # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases +if os.environ.get("DEVELOPMENT_MODE"): + host = "localhost" +else: + host = os.environ.get("DB_HOST") + + DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ.get("DB_NAME"), - "USER": os.environ.get("DB_USERNAME"), - "PASSWORD": os.environ.get("DB_PASSWORD"), - "HOST": os.environ.get("DB_HOST"), - "PORT": os.environ.get("DB_PORT"), - } + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("DB_NAME"), + "USER": os.environ.get("DB_USERNAME"), + "PASSWORD": os.environ.get("DB_PASSWORD"), + "HOST": host, + "PORT": os.environ.get("DB_PORT"), } +} print("Connected to database") @@ -128,7 +133,7 @@ # } # print("Connected to database") -AUTH_USER_MODEL = 'app.User' +AUTH_USER_MODEL = "app.User" # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators @@ -184,7 +189,7 @@ "rest_framework_simplejwt.authentication.JWTAuthentication", "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.BasicAuthentication", - "app.authentication.ProjectAPIKeyAuthentication" + "app.authentication.ProjectAPIKeyAuthentication", ), "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", From b306c33b4a3eaf1ae888487554b92374fc0e322e Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 14:46:16 +0000 Subject: [PATCH 04/11] Add explicit null and blank == false flags to model fields --- backend/app/models/environment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/models/environment.py b/backend/app/models/environment.py index d114032..5643f40 100644 --- a/backend/app/models/environment.py +++ b/backend/app/models/environment.py @@ -4,12 +4,12 @@ class Environment(models.Model): - name = models.CharField(max_length=100) - identifier = models.CharField(max_length=255, default=uuid.uuid4) + name = models.CharField(max_length=100, blank=False, null=False) + identifier = models.CharField(max_length=255, default=uuid.uuid4, blank=False, null=False) project = models.ForeignKey( "Project", on_delete=models.CASCADE, related_name="environments" ) - is_default = models.BooleanField(default=False) + is_default = models.BooleanField(default=False, blank=False, null=False) class Meta: unique_together = ["identifier", "project"] From 4c47498a6ecbd82cc3ad3c72dd1ba268e4f72cb8 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 14:47:30 +0000 Subject: [PATCH 05/11] Add environment serializer and update project serializer + view --- backend/app/serializers/environment_serializer.py | 9 +++++++++ backend/app/serializers/project_serializer.py | 10 ++++++++-- backend/app/views/project_views.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 backend/app/serializers/environment_serializer.py diff --git a/backend/app/serializers/environment_serializer.py b/backend/app/serializers/environment_serializer.py new file mode 100644 index 0000000..e094ce5 --- /dev/null +++ b/backend/app/serializers/environment_serializer.py @@ -0,0 +1,9 @@ +from app.models import Environment +from rest_framework import serializers + + +class EnvironmentSerializer(serializers.ModelSerializer): + class Meta: + model = Environment + fields = ["id", "name", "identifier", "is_default"] + extra_kwargs = {'id': {'required': True}, 'name': {'required': True}, 'identifier': {'required': True}, 'is_default': {'required': True} } diff --git a/backend/app/serializers/project_serializer.py b/backend/app/serializers/project_serializer.py index f8c2348..a3fa6a0 100644 --- a/backend/app/serializers/project_serializer.py +++ b/backend/app/serializers/project_serializer.py @@ -1,9 +1,11 @@ -from app.models import Project +from app.models import Project, Environment from app.models.project import ProjectMembership from .user_serializer import UserSummarySerializer +from .environment_serializer import EnvironmentSerializer from rest_framework import serializers + class ProjectMembershipSerializer(serializers.ModelSerializer): """Used as a nested serializer by ApplicationSerializer.""" @@ -21,10 +23,11 @@ class ProjectSerializer(serializers.ModelSerializer): source="projectmembership_set.all", # default related name for ProjectMembership. read_only=True, ) + environments = EnvironmentSerializer(many=True, read_only=True) class Meta: model = Project - fields = ["id", "name", "api_key", "members"] + fields = ["id", "name", "api_key", "members", "environments"] def to_representation(self, instance): """ @@ -37,5 +40,8 @@ def to_representation(self, instance): # Serialize the `members` using `ProjectMembershipSerializer`. membership_qs = ProjectMembership.objects.filter(project=instance) representation["members"] = ProjectMembershipSerializer(membership_qs, many=True).data + # Serialize the 'environments' using 'EnvironmentSerializer' + environment_qs = Environment.objects.filter(project=instance) + representation["environments"] = EnvironmentSerializer(environment_qs, many=True).data return representation diff --git a/backend/app/views/project_views.py b/backend/app/views/project_views.py index e5bec52..cc89dad 100644 --- a/backend/app/views/project_views.py +++ b/backend/app/views/project_views.py @@ -13,7 +13,7 @@ class ProjectsView(viewsets.ModelViewSet): permission_classes = [IsAuthenticated, ProjectMemberPermission] def get_queryset(self): - return self.queryset.filter(members__pk=self.request.user.pk) + return self.queryset.filter(members__pk=self.request.user.pk).prefetch_related('environments') def get_object(self): # Fetch the object and check if the request user has the necessary permissions. From 8ca2ae30b79f1d8f6d7c325c29bd979057aa4778 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 14:48:15 +0000 Subject: [PATCH 06/11] Fix database host when in DEVELOPMENT_MODE --- backend/backend/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 2b74bd4..49a3755 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -87,9 +87,11 @@ # Database # https://docs.djangoproject.com/en/4.1/ref/settings/#databases -if os.environ.get("DEVELOPMENT_MODE"): +if DEVELOPMENT_MODE: + print("DEVELOPMENT MODE: ACTIVE") host = "localhost" else: + print("DEVELOPMENT MODE: INACTIVE") host = os.environ.get("DB_HOST") From e2010106e510febe4a7a9dce8e139cbcab3e3d63 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 14:48:59 +0000 Subject: [PATCH 07/11] Add generated models schema and TS interfaces --- frontend/openapi-schema.yml | 21 +++++++++++++++++++ frontend/src/api/schema/api.ts | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/frontend/openapi-schema.yml b/frontend/openapi-schema.yml index e462adb..40faf87 100644 --- a/frontend/openapi-schema.yml +++ b/frontend/openapi-schema.yml @@ -319,6 +319,27 @@ components: required: - user readOnly: true + environments: + type: array + items: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + identifier: + type: string + maxLength: 255 + is_default: + type: boolean + required: + - name + - identifier + - is_default + readOnly: true required: - name TokenObtainPair: diff --git a/frontend/src/api/schema/api.ts b/frontend/src/api/schema/api.ts index e06797f..2901b66 100644 --- a/frontend/src/api/schema/api.ts +++ b/frontend/src/api/schema/api.ts @@ -53,6 +53,43 @@ export interface Project { * @memberof Project */ 'members'?: Array; + /** + * + * @type {Array} + * @memberof Project + */ + 'environments'?: Array; +} +/** + * + * @export + * @interface ProjectEnvironmentsInner + */ +export interface ProjectEnvironmentsInner { + /** + * + * @type {number} + * @memberof ProjectEnvironmentsInner + */ + 'id'?: number; + /** + * + * @type {string} + * @memberof ProjectEnvironmentsInner + */ + 'name': string; + /** + * + * @type {string} + * @memberof ProjectEnvironmentsInner + */ + 'identifier': string; + /** + * + * @type {boolean} + * @memberof ProjectEnvironmentsInner + */ + 'is_default': boolean; } /** * From 5367f2d33533de75c4182d7cb60124934cf41190 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 14:49:27 +0000 Subject: [PATCH 08/11] Use the projects environments in the picker --- frontend/src/components/picker/index.tsx | 1 - frontend/src/components/sidebarMenu/index.tsx | 15 ++++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/picker/index.tsx b/frontend/src/components/picker/index.tsx index 72b78bb..adfbd46 100644 --- a/frontend/src/components/picker/index.tsx +++ b/frontend/src/components/picker/index.tsx @@ -3,7 +3,6 @@ import { Listbox, Transition } from '@headlessui/react'; import { MdUnfoldMore, MdOutlineCheck } from 'react-icons/md'; interface Item { - id: number; name: string; } diff --git a/frontend/src/components/sidebarMenu/index.tsx b/frontend/src/components/sidebarMenu/index.tsx index a1a0d66..71e41d9 100644 --- a/frontend/src/components/sidebarMenu/index.tsx +++ b/frontend/src/components/sidebarMenu/index.tsx @@ -22,11 +22,6 @@ import { useNavigate } from 'react-router'; import { isHasData } from 'api/utils'; import { usePopper } from 'react-popper'; -const environments = [ - { id: 1, name: 'Development' }, - { id: 2, name: 'Production' }, -]; - interface ProjectEntry { id: number; name: string; @@ -87,7 +82,13 @@ const SidebarMenu = ({ projects }: SidebarMenuProps) => {
Crumpet
- + {isHasData(selectedProject) && selectedProject.data.environments != undefined && ( + + )}
{ButtonList.map((button, index) => ( { ref={setPopperElement} style={styles.popper} {...attributes.popper} - className="w-full px-2"> + className="w-full px-2">
{projects.map((project, index) => ( From 22089e4e10ad64908e339621c47fcbc09d5e6a9e Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 14:50:09 +0000 Subject: [PATCH 09/11] Add VScode settings for test runner and env file --- .vscode/settings.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d04735..2697427 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,14 @@ { + "python.envFile": "${workspaceFolder}/backend/.env", "prettier.configPath": "./frontend/.prettierrc.js", - "prettier.bracketSpacing": false + "prettier.bracketSpacing": false, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./backend", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true } From 92b5d757ca12aeda375de6315cb1b8b703511e9a Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 15:47:14 +0000 Subject: [PATCH 10/11] Create new prod .env file and update docker compose --- backend/.env.production | 11 +++++++++++ backend/docker-compose.yml | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 backend/.env.production diff --git a/backend/.env.production b/backend/.env.production new file mode 100644 index 0000000..69d4e37 --- /dev/null +++ b/backend/.env.production @@ -0,0 +1,11 @@ +DEBUG=False +DEVELOPMENT_MODE=False +DJANGO_ALLOWED_HOSTS='localhost,0.0.0.0,127.0.0.1,192.168.1.220' +DJANGO_SETTINGS_MODULE=backend.settings + +DB_NAME = crumpet_db +DB_USERNAME = head_baker +DB_PASSWORD = Crumpet2023 +DB_HOST = db +DB_PORT = 5432 +DB_SSL_MODE = require diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 45a7a58..a4b2ff9 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -10,8 +10,10 @@ services: ports: - "8000:8000" tty: true + environment: + - ENVIRONMENT=production env_file: - - .env + - .env.production depends_on: - db From 61c87ace90ef635563e0b523636c3a04584da993 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Fri, 3 Nov 2023 15:47:52 +0000 Subject: [PATCH 11/11] Move .env to .env.development and update settings.py --- backend/{.env => .env.development} | 0 backend/backend/settings.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) rename backend/{.env => .env.development} (100%) diff --git a/backend/.env b/backend/.env.development similarity index 100% rename from backend/.env rename to backend/.env.development diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 49a3755..1e94ad4 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -17,13 +17,17 @@ from dotenv import load_dotenv import os -LOAD_DOTENV = os.getenv("LOAD_DOTENV", "True") == "True" -if LOAD_DOTENV: - load_dotenv() - # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') # Assume development by default +if ENVIRONMENT == 'production': + dotenv_path = BASE_DIR / '.env.production' +else: + dotenv_path = BASE_DIR / '.env.development' + +if dotenv_path.exists(): + load_dotenv(dotenv_path=dotenv_path) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/