forked from Hostea/dashboard
feat: add instance view
parent
3705c64616
commit
d15eb7ae3d
|
@ -1,5 +1,6 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import InstanceConfiguration
|
from .models import InstanceConfiguration, Instance
|
||||||
|
|
||||||
admin.site.register(InstanceConfiguration)
|
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"
|
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>
|
<p class="secondary-nav__option-link">Hello, {{ username }}!</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="secondary-nav__options">
|
<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 }}>
|
<details class="secondary-nav__options-group" {{ open_support }}>
|
||||||
<summary><a href="{% url 'support.home' %}">Support</a></summary>
|
<summary><a href="{% url 'support.home' %}">Support</a></summary>
|
||||||
<div class="secondary-nav__options-group-options">
|
<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.conf import settings
|
||||||
from django.db.utils import IntegrityError
|
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):
|
class DashHome(TestCase):
|
||||||
|
@ -28,15 +66,7 @@ class DashHome(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.password = "password121231"
|
register_util(t=self, username="dashboard_home_user")
|
||||||
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()
|
|
||||||
|
|
||||||
def test_dash_is_protected(self):
|
def test_dash_is_protected(self):
|
||||||
"""
|
"""
|
||||||
|
@ -60,13 +90,7 @@ class DashHome(TestCase):
|
||||||
c = Client()
|
c = Client()
|
||||||
|
|
||||||
# username login works
|
# username login works
|
||||||
payload = {
|
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||||
"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"))
|
|
||||||
|
|
||||||
# email login works
|
# email login works
|
||||||
resp = c.get(reverse("dash.home"))
|
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
|
name="test config 3", ram=0.5, cpu=1, storage=0.5
|
||||||
)
|
)
|
||||||
config3.save()
|
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.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
from .views import home
|
from .views import home, create_instance
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("instance/new/", create_instance, name="dash.instances.new"),
|
||||||
path("", home, name="dash.home"),
|
path("", home, name="dash.home"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,6 +21,8 @@ from django.http import HttpResponse
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from .models import Instance, InstanceConfiguration
|
||||||
|
|
||||||
|
|
||||||
def default_ctx(title: str, username: str):
|
def default_ctx(title: str, username: str):
|
||||||
"""
|
"""
|
||||||
|
@ -38,7 +40,58 @@ def home(request):
|
||||||
Dashboard homepage view
|
Dashboard homepage view
|
||||||
"""
|
"""
|
||||||
PAGE_TITLE = "Home"
|
PAGE_TITLE = "Home"
|
||||||
username = request.user
|
user = request.user
|
||||||
ctx = default_ctx(title=PAGE_TITLE, username=username.username)
|
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)
|
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: " ►";
|
content: " ►";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.secondary-nav__options-group[open] > summary::after {
|
.secondary-nav__options-group[open] > summary::after {
|
||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
content: " ▼";
|
content: " ▼";
|
||||||
|
@ -531,11 +530,72 @@ footer {
|
||||||
/* secondary nav ends */
|
/* secondary nav ends */
|
||||||
|
|
||||||
.dash__main {
|
.dash__main {
|
||||||
flex: 2;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: calc(-63px + 100vh);
|
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 */
|
/* dashbaord ends */
|
||||||
|
|
Loading…
Reference in New Issue