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 "Go back" %} + +

{% 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.

+ + + + + + + + + + + + + + {% for record in object.records %} + + + + + {% endfor %} + +
{% trans "Type" %}{% trans "Value" %}
{{ record.type }}{{ record.value }}
+{% 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