diff --git a/musician/api.py b/musician/api.py
index 0536b5c..4df4943 100644
--- a/musician/api.py
+++ b/musician/api.py
@@ -2,7 +2,9 @@ import requests
import urllib.parse
from django.conf import settings
+from django.http import Http404
from django.urls.exceptions import NoReverseMatch
+from django.utils.translation import gettext_lazy as _
from .models import Domain, DatabaseService, MailService, SaasService, UserAccount
@@ -18,6 +20,7 @@ API_PATHS = {
# services
'database-list': 'databases/',
'domain-list': 'domains/',
+ 'domain-detail': 'domains/{pk}/',
'address-list': 'addresses/',
'mailbox-list': 'mailboxes/',
'mailinglist-list': 'lists/',
@@ -55,9 +58,13 @@ class Orchestra(object):
return response.json().get("token", None)
- def request(self, verb, resource, querystring=None, raise_exception=True):
+ def request(self, verb, resource=None, querystring=None, url=None, raise_exception=True):
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
- url = self.build_absolute_uri(resource)
+ if resource is not None:
+ url = self.build_absolute_uri(resource)
+ elif url is None:
+ raise AttributeError("Provide `resource` or `url` params")
+
if querystring is not None:
url = "{}?{}".format(url, querystring)
@@ -86,6 +93,15 @@ class Orchestra(object):
raise PermissionError("Cannot retrieve profile of an anonymous user.")
return UserAccount.new_from_json(output[0])
+ def retrieve_domain(self, pk):
+ path = API_PATHS.get('domain-detail').format_map({'pk': pk})
+
+ url = urllib.parse.urljoin(self.base_url, path)
+ status, domain_json = self.request("GET", url=url, raise_exception=False)
+ if status == 404:
+ raise Http404(_("No domain found matching the query"))
+ return Domain.new_from_json(domain_json)
+
def retrieve_domain_list(self):
output = self.retrieve_service_list(Domain.api_name)
domains = []
diff --git a/musician/static/musician/css/default.css b/musician/static/musician/css/default.css
index b2fb536..42972ab 100644
--- a/musician/static/musician/css/default.css
+++ b/musician/static/musician/css/default.css
@@ -7,6 +7,32 @@ a:hover {
color: rgba(0,0,0,.7);
}
+.btn-arrow-left{
+ color: #eee;
+ background: #D3D0DA;
+ position: relative;
+ padding: 8px 20px 8px 30px;
+ margin-left: 1em; /** equal value than arrow.left **/
+}
+
+.btn-arrow-left::after,
+.btn-arrow-left::before{
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: -1em;
+
+ margin-top: -19px;
+ border-top: 19px solid transparent;
+ border-bottom: 19px solid transparent;
+ border-right: 1em solid;
+}
+
+.btn-arrow-left::after{
+ border-right-color: #D3D0DA;
+ z-index: 2;
+}
+
.wrapper {
display: flex;
width: 100%;
diff --git a/musician/templates/musician/dashboard.html b/musician/templates/musician/dashboard.html
index 53c694e..d621e1c 100644
--- a/musician/templates/musician/dashboard.html
+++ b/musician/templates/musician/dashboard.html
@@ -40,7 +40,8 @@
@@ -120,7 +121,7 @@
@@ -137,6 +138,7 @@ $('#configDetailsModal').on('show.bs.modal', function (event) {
modal.find('.modal-body #config-username').text(button.data('username'));
modal.find('.modal-body #config-password').text(button.data('password'));
modal.find('.modal-body #config-root').text(button.data('root'));
+ modal.find('.modal-footer .btn').attr('href', button.data('url'));
})
{% endblock %}
diff --git a/musician/templates/musician/domain_detail.html b/musician/templates/musician/domain_detail.html
new file mode 100644
index 0000000..515a724
--- /dev/null
+++ b/musician/templates/musician/domain_detail.html
@@ -0,0 +1,30 @@
+{% extends "musician/base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+
+{% trans "DNS settings for" %} {{ object.name }}
+Litle description of what to be expected in this section to aid the user. Even a link to more help if there is one available.
+
+
+
+
+
+
+
+
+ {% trans "Type" %} |
+ {% trans "Value" %} |
+
+
+
+ {% for record in object.records %}
+
+ {{ record.type }} |
+ {{ record.value }} |
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/musician/tests.py b/musician/tests.py
index 7ce503c..b25e23a 100644
--- a/musician/tests.py
+++ b/musician/tests.py
@@ -1,3 +1,14 @@
from django.test import TestCase
-# Create your tests here.
+
+class DomainsTestCase(TestCase):
+ def test_domain_not_found(self):
+ response = self.client.post(
+ '/auth/login/',
+ {'username': 'admin', 'password': 'admin'},
+ follow=True
+ )
+ self.assertEqual(200, response.status_code)
+
+ response = self.client.get('/domains/3/')
+ self.assertEqual(404, response.status_code)
diff --git a/musician/urls.py b/musician/urls.py
index 340d409..6bae570 100644
--- a/musician/urls.py
+++ b/musician/urls.py
@@ -15,6 +15,7 @@ urlpatterns = [
path('auth/login/', views.LoginView.as_view(), name='login'),
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
+ path('domains//', views.DomainDetailView.as_view(), name='domain-detail'),
path('billing/', views.BillingView.as_view(), name='billing'),
path('profile/', views.ProfileView.as_view(), name='profile'),
path('mails/', views.MailView.as_view(), name='mails'),
diff --git a/musician/views.py b/musician/views.py
index b6b4298..ae3b019 100644
--- a/musician/views.py
+++ b/musician/views.py
@@ -1,4 +1,3 @@
-
from itertools import groupby
from django.core.exceptions import ImproperlyConfigured
@@ -186,6 +185,25 @@ class SaasView(ServiceListView):
template_name = "musician/saas.html"
+class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
+ template_name = "musician/domain_detail.html"
+
+ def get_queryset(self):
+ # Return an empty list to avoid a request to retrieve all the
+ # user domains. We will get a 404 if the domain doesn't exists
+ # while invoking `get_object`
+ return []
+
+ def get_object(self, queryset=None):
+ if queryset is None:
+ queryset = self.get_queryset()
+
+ pk = self.kwargs.get(self.pk_url_kwarg)
+ domain = self.orchestra.retrieve_domain(pk)
+
+ return domain
+
+
class LoginView(FormView):
template_name = 'auth/login.html'
form_class = LoginForm