Skip to content

Commit

Permalink
Merge pull request #21 from CrumpetDev/setup/environments
Browse files Browse the repository at this point in the history
Setup/environments
  • Loading branch information
tomtitherington authored Nov 3, 2023
2 parents 49278b7 + 61c87ac commit 62063cc
Show file tree
Hide file tree
Showing 20 changed files with 290 additions and 28 deletions.
12 changes: 11 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions backend/.env → backend/.env.development
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 11 additions & 0 deletions backend/.env.production
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'app.apps.AppConfig'
3 changes: 3 additions & 0 deletions backend/app/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class AppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app'

def ready(self):
import app.signals
42 changes: 42 additions & 0 deletions backend/app/migrations/0007_environment.py
Original file line number Diff line number Diff line change
@@ -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")},
},
),
]
1 change: 1 addition & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
from .user import User
from .project import Project, ProjectMembership
from .selector import Selector
from .environment import Environment
24 changes: 24 additions & 0 deletions backend/app/models/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db import models

import uuid


class Environment(models.Model):
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, blank=False, null=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
9 changes: 9 additions & 0 deletions backend/app/serializers/environment_serializer.py
Original file line number Diff line number Diff line change
@@ -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} }
10 changes: 8 additions & 2 deletions backend/app/serializers/project_serializer.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand All @@ -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):
"""
Expand All @@ -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
11 changes: 11 additions & 0 deletions backend/app/signals.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions backend/app/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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 *
68 changes: 68 additions & 0 deletions backend/app/tests/test_environments.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 1 addition & 1 deletion backend/app/views/project_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 26 additions & 15 deletions backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -84,20 +88,27 @@
WSGI_APPLICATION = "backend.wsgi.application"



# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

if DEVELOPMENT_MODE:
print("DEVELOPMENT MODE: ACTIVE")
host = "localhost"
else:
print("DEVELOPMENT MODE: INACTIVE")
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")


Expand Down Expand Up @@ -128,7 +139,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
Expand Down Expand Up @@ -184,7 +195,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",
Expand Down
4 changes: 3 additions & 1 deletion backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ services:
ports:
- "8000:8000"
tty: true
environment:
- ENVIRONMENT=production
env_file:
- .env
- .env.production
depends_on:
- db

Expand Down
21 changes: 21 additions & 0 deletions frontend/openapi-schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 62063cc

Please sign in to comment.