feat: add instance view
parent
3705c64616
commit
d15eb7ae3d
|
@ -1,5 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import InstanceConfiguration
|
||||
from .models import InstanceConfiguration, Instance
|
||||
|
||||
admin.site.register(InstanceConfiguration)
|
||||
admin.site.register(Instance)
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# Generated by Django 4.0.3 on 2022-06-17 15:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("dash", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Instance",
|
||||
fields=[
|
||||
("ID", models.AutoField(primary_key=True, serialize=False)),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
max_length=32,
|
||||
unique=True,
|
||||
verbose_name="Name of this Instance. Also Serves as subdomain",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"configuration_id",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="dash.instanceconfiguration",
|
||||
),
|
||||
),
|
||||
(
|
||||
"owned_by",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.0.3 on 2022-06-17 17:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dash", "0002_instance"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="instance",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
max_length=200,
|
||||
unique=True,
|
||||
verbose_name="Name of this Instance. Also Serves as subdomain",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -50,3 +50,25 @@ class InstanceConfiguration(models.Model):
|
|||
fields=["ram", "cpu", "storage"], name="no_repeat_config"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class Instance(models.Model):
|
||||
"""
|
||||
Hostea instances
|
||||
"""
|
||||
|
||||
owned_by = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
ID = models.AutoField(primary_key=True)
|
||||
name = models.CharField(
|
||||
"Name of this Instance. Also Serves as subdomain",
|
||||
unique=True,
|
||||
max_length=200,
|
||||
)
|
||||
configuration_id = models.ForeignKey(
|
||||
InstanceConfiguration, on_delete=models.PROTECT
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.owned_by}'s instance '{self.name}'"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<p class="secondary-nav__option-link">Hello, {{ username }}!</p>
|
||||
</div>
|
||||
<div class="secondary-nav__options">
|
||||
<a href="/foo" class="secondary-nav__option-link">Instances</a>
|
||||
<a href="{% url 'dash.instances.new' %}" class="secondary-nav__option-link">Instances</a>
|
||||
<details class="secondary-nav__options-group" {{ open_support }}>
|
||||
<summary><a href="{% url 'support.home' %}">Support</a></summary>
|
||||
<div class="secondary-nav__options-group-options">
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
{% extends 'dash/common/base.html' %} {% block dash %}
|
||||
<h2>{{ title }}</h2>
|
||||
<form
|
||||
action="{% url 'dash.instances.new' %}"
|
||||
method="POST"
|
||||
class="dash__form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<label class="dash__form-label" for="login">
|
||||
Instance name
|
||||
<input
|
||||
class="form__input"
|
||||
name="name"
|
||||
autofocus
|
||||
required
|
||||
id="name"
|
||||
type="text"
|
||||
{% if name %}
|
||||
value="{{ name }}"
|
||||
{% endif %}
|
||||
/>
|
||||
|
||||
</label>
|
||||
|
||||
<fieldset class="configuration">
|
||||
<legend>Instance Configuration</legend>
|
||||
{% for config in configurations %}
|
||||
<label for="{{ config.name }}">
|
||||
<div class="configuration__container">
|
||||
<h3 class="configuration__name">{{ config.name }}</h3>
|
||||
<ul>
|
||||
<li class="configuration__spec">{{ config.cpu }} vCPU</li>
|
||||
<li class="configuration__spec">{{ config.ram }} GB RAM</li>
|
||||
<li class="configuration__spec">{{ config.storage }} GB Storage</li>
|
||||
</ul>
|
||||
<input
|
||||
required
|
||||
type="radio"
|
||||
id="{{ config.name }}"
|
||||
name="configuration"
|
||||
value="{{ config.name }}"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit" type="submit">Create Instance</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
109
dash/tests.py
109
dash/tests.py
|
@ -19,7 +19,45 @@ from django.test import TestCase, Client, override_settings
|
|||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from .models import InstanceConfiguration
|
||||
from .models import InstanceConfiguration, Instance
|
||||
|
||||
|
||||
def register_util(t: TestCase, username: str):
|
||||
t.password = "password121231"
|
||||
t.username = username
|
||||
t.email = f"{t.username}@example.org"
|
||||
t.user = get_user_model().objects.create(
|
||||
username=t.username,
|
||||
email=t.email,
|
||||
)
|
||||
t.user.set_password(t.password)
|
||||
t.user.save()
|
||||
|
||||
|
||||
def create_configurations(t: TestCase):
|
||||
t.instance_config = [
|
||||
InstanceConfiguration(name="Personal", ram=0.5, cpu=1, storage=25),
|
||||
InstanceConfiguration(name="Enthusiast", ram=2, cpu=2, storage=50),
|
||||
InstanceConfiguration(name="Small Business", ram=8, cpu=4, storage=64),
|
||||
InstanceConfiguration(name="Enterprise", ram=64, cpu=24, storage=1024),
|
||||
]
|
||||
|
||||
for instance in t.instance_config:
|
||||
instance.save()
|
||||
print(f"[*][init] Instance {instance.name} is saved")
|
||||
t.assertEqual(
|
||||
InstanceConfiguration.objects.filter(name=instance.name).exists(), True
|
||||
)
|
||||
|
||||
|
||||
def login_util(t: TestCase, c: Client, redirect_to: str):
|
||||
payload = {
|
||||
"login": t.username,
|
||||
"password": t.password,
|
||||
}
|
||||
resp = c.post(reverse("accounts.login"), payload)
|
||||
t.assertEqual(resp.status_code, 302)
|
||||
t.assertEqual(resp.headers["location"], reverse(redirect_to))
|
||||
|
||||
|
||||
class DashHome(TestCase):
|
||||
|
@ -28,15 +66,7 @@ class DashHome(TestCase):
|
|||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.password = "password121231"
|
||||
self.username = "dashboard_home_user"
|
||||
self.email = f"{self.username}@example.org"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
email=self.email,
|
||||
)
|
||||
self.user.set_password(self.password)
|
||||
self.user.save()
|
||||
register_util(t=self, username="dashboard_home_user")
|
||||
|
||||
def test_dash_is_protected(self):
|
||||
"""
|
||||
|
@ -60,13 +90,7 @@ class DashHome(TestCase):
|
|||
c = Client()
|
||||
|
||||
# username login works
|
||||
payload = {
|
||||
"login": self.username,
|
||||
"password": self.password,
|
||||
}
|
||||
resp = c.post(reverse("accounts.login"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
|
||||
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||
|
||||
# email login works
|
||||
resp = c.get(reverse("dash.home"))
|
||||
|
@ -98,3 +122,54 @@ class InstancesConfig(TestCase):
|
|||
name="test config 3", ram=0.5, cpu=1, storage=0.5
|
||||
)
|
||||
config3.save()
|
||||
|
||||
|
||||
class CreateInstance(TestCase):
|
||||
def setUp(self):
|
||||
register_util(t=self, username="createinstance_user")
|
||||
create_configurations(t=self)
|
||||
|
||||
def test_create_instance_renders(self):
|
||||
c = Client()
|
||||
login_util(self, c, "accounts.home")
|
||||
urls = [(reverse("dash.instances.new"), "Instance Configuration")]
|
||||
for (url, test) in urls:
|
||||
print(f"[*] Testing URI: {url}")
|
||||
resp = c.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(b"Billing" in resp.content, True)
|
||||
self.assertEqual(b"Support" in resp.content, True)
|
||||
self.assertEqual(b"Logout" in resp.content, True)
|
||||
self.assertEqual(str.encode(test) in resp.content, True)
|
||||
|
||||
payload = {
|
||||
"name": "test_create_instance_renders",
|
||||
"configuration": self.instance_config[0].name,
|
||||
}
|
||||
|
||||
self.assertEqual(Instance.objects.filter(name=payload["name"]).exists(), False)
|
||||
|
||||
resp = c.post(reverse("dash.instances.new"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.home"))
|
||||
|
||||
self.assertEqual(
|
||||
Instance.objects.filter(
|
||||
name=payload["name"],
|
||||
owned_by=self.user,
|
||||
configuration_id=self.instance_config[0],
|
||||
).exists(),
|
||||
True,
|
||||
)
|
||||
|
||||
resp = c.post(reverse("dash.instances.new"), payload)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(b"Instance name exists" in resp.content, True)
|
||||
|
||||
payload = {
|
||||
"name": f"2{payload['name']}",
|
||||
"configuration": f"2{payload['name']}",
|
||||
}
|
||||
resp = c.post(reverse("dash.instances.new"), payload)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(b"Configuration doesn" in resp.content, True)
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
from .views import home
|
||||
from .views import home, create_instance
|
||||
|
||||
urlpatterns = [
|
||||
path("instance/new/", create_instance, name="dash.instances.new"),
|
||||
path("", home, name="dash.home"),
|
||||
]
|
||||
|
|
|
@ -21,6 +21,8 @@ from django.http import HttpResponse
|
|||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import Instance, InstanceConfiguration
|
||||
|
||||
|
||||
def default_ctx(title: str, username: str):
|
||||
"""
|
||||
|
@ -38,7 +40,58 @@ def home(request):
|
|||
Dashboard homepage view
|
||||
"""
|
||||
PAGE_TITLE = "Home"
|
||||
username = request.user
|
||||
ctx = default_ctx(title=PAGE_TITLE, username=username.username)
|
||||
user = request.user
|
||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||
|
||||
instances = Instance.objects.filter(owned_by=user)
|
||||
if instances:
|
||||
ctx["instances"] = instances
|
||||
|
||||
return render(request, "dash/home/index.html", context=ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def create_instance(request):
|
||||
"""
|
||||
Create instance view
|
||||
"""
|
||||
PAGE_TITLE = "Create Instance"
|
||||
user = request.user
|
||||
|
||||
def get_ctx():
|
||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||
configurations = InstanceConfiguration.objects.all()
|
||||
ctx["configurations"] = InstanceConfiguration.objects.all()
|
||||
return ctx
|
||||
|
||||
if request.method == "GET":
|
||||
ctx = get_ctx()
|
||||
return render(request, "dash/instances/new/index.html", context=ctx)
|
||||
|
||||
name = request.POST["name"]
|
||||
if Instance.objects.filter(name=name).exists():
|
||||
ctx = get_ctx()
|
||||
ctx["error"] = {
|
||||
"title": "Can't create instance",
|
||||
"reason": "Instance name exists, please try again with a different name",
|
||||
}
|
||||
print(ctx["error"]["reason"])
|
||||
return render(request, "dash/instances/new/index.html", status=400, context=ctx)
|
||||
|
||||
configuration = request.POST["configuration"]
|
||||
if not InstanceConfiguration.objects.filter(name=configuration).exists():
|
||||
ctx = get_ctx()
|
||||
ctx["error"] = {
|
||||
"title": "Can't create instance",
|
||||
"reason": "Configuration doesn't exist, please try again.",
|
||||
}
|
||||
print(ctx["error"]["reason"])
|
||||
return render(request, "dash/instances/new/index.html", status=400, context=ctx)
|
||||
|
||||
configuration = get_object_or_404(InstanceConfiguration, name=configuration)
|
||||
instance = Instance(
|
||||
name=name, configuration_id=configuration, owned_by=request.user
|
||||
)
|
||||
instance.save()
|
||||
return redirect(reverse("dash.home"))
|
||||
|
|
|
@ -522,7 +522,6 @@ footer {
|
|||
content: " ►";
|
||||
}
|
||||
|
||||
|
||||
.secondary-nav__options-group[open] > summary::after {
|
||||
margin-left: 30px;
|
||||
content: " ▼";
|
||||
|
@ -531,11 +530,72 @@ footer {
|
|||
/* secondary nav ends */
|
||||
|
||||
.dash__main {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: calc(-63px + 100vh);
|
||||
flex:2;
|
||||
}
|
||||
|
||||
/* dash forms starts */
|
||||
.dash__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
margin: auto;
|
||||
background-color: ping;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.configuration__container {
|
||||
padding: 20px;
|
||||
padding-left: 0;
|
||||
margin: 10px 20px;
|
||||
margin-left: 0;
|
||||
border: 1px solid rgb(211, 211, 211);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.configuration__name {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.configuration__spec {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
.form__label {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.form__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
padding: 5px 0;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.form__submit {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 400;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
background-color: rgb(0, 128, 0);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/* dash forms ends */
|
||||
|
||||
/* dashbaord ends */
|
||||
|
|
Loading…
Reference in New Issue