mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-06-03 10:50:20 +00:00
dynamicdns: Implement adding multiple domains
Tests: - Functional tests pass. - Adding domain triggers domain_added signal. - Editing a domain triggers domain removed and domain added signals. - Deleting a domain trigger domain removed signal. - For each of the action, the status table shows updated information. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
6d2f992a42
commit
4176f53e05
@ -12,8 +12,8 @@ from plinth import cfg
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
|
||||
class ConfigureForm(forms.Form):
|
||||
"""Form to configure the Dynamic DNS client."""
|
||||
class DomainForm(forms.Form):
|
||||
"""Form to add/edit a domain in the Dynamic DNS client."""
|
||||
help_update_url = \
|
||||
gettext_lazy('The Variables <User>, <Pass>, <Ip>, '
|
||||
'<Domain> may be used within the URL. For details '
|
||||
|
||||
@ -28,29 +28,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const FREEDNS = 'https://freedns.afraid.org/dynamic/update.php?' +
|
||||
'_YOURAPIKEYHERE_';
|
||||
|
||||
document.getElementById('id_service_type').addEventListener('change', () => {
|
||||
document.getElementById('id_domain-service_type').addEventListener('change', () => {
|
||||
setMode();
|
||||
|
||||
const service_type = document.getElementById('id_service_type').value;
|
||||
const service_type = document.getElementById('id_domain-service_type').value;
|
||||
if (service_type === "noip.com") {
|
||||
document.getElementById('id_update_url').value = NOIP;
|
||||
document.getElementById('id_domain-update_url').value = NOIP;
|
||||
} else if (service_type === "freedns.afraid.org") {
|
||||
document.getElementById('id_update_url').value = FREEDNS;
|
||||
document.getElementById('id_domain-update_url').value = FREEDNS;
|
||||
} else { // GnuDIP and other
|
||||
document.getElementById('id_update_url').value = '';
|
||||
document.getElementById('id_domain-update_url').value = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('id_show_password').addEventListener('change', () => {
|
||||
if (document.getElementById('id_show_password').checked) {
|
||||
document.getElementById('id_password').type = 'text';
|
||||
document.getElementById('id_domain-show_password').addEventListener('change', () => {
|
||||
if (document.getElementById('id_domain-show_password').checked) {
|
||||
document.getElementById('id_domain-password').type = 'text';
|
||||
} else {
|
||||
document.getElementById('id_password').type = 'password';
|
||||
document.getElementById('id_domain-password').type = 'password';
|
||||
}
|
||||
});
|
||||
|
||||
function setMode() {
|
||||
const service_type = document.getElementById('id_service_type').value;
|
||||
const service_type = document.getElementById('id_domain-service_type').value;
|
||||
if (service_type === "gnudip") {
|
||||
setGnudipMode();
|
||||
} else {
|
||||
@ -62,19 +62,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.form-group').forEach((element) => {
|
||||
element.style.display = 'block';
|
||||
});
|
||||
document.getElementById('id_update_url').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_disable_ssl_cert_check').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_use_http_basic_auth').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_use_ipv6').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_server').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_domain-update_url').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_domain-disable_ssl_cert_check').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_domain-use_http_basic_auth').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_domain-use_ipv6').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_domain-server').closest('.form-group').style.display = 'block';
|
||||
}
|
||||
|
||||
function setUpdateUrlMode() {
|
||||
document.getElementById('id_update_url').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_disable_ssl_cert_check').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_use_http_basic_auth').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_use_ipv6').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_server').closest('.form-group').style.display = 'none';
|
||||
document.getElementById('id_domain-update_url').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_domain-disable_ssl_cert_check').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_domain-use_http_basic_auth').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_domain-use_ipv6').closest('.form-group').style.display = 'block';
|
||||
document.getElementById('id_domain-server').closest('.form-group').style.display = 'none';
|
||||
}
|
||||
|
||||
setMode();
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{{ title }}</h3>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
App configurations will be updated.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form class="form form-delete" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="submit" class="btn btn-md btn-danger"
|
||||
value="{% blocktrans %}Delete{% endblocktrans %}"/>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
11
plinth/modules/dynamicdns/templates/dynamicdns-domain.html
Normal file
11
plinth/modules/dynamicdns/templates/dynamicdns-domain.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "form.html" %}
|
||||
{% comment %}
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'dynamicdns/dynamicdns.js' %}"
|
||||
defer></script>
|
||||
{% endblock %}
|
||||
@ -5,18 +5,20 @@
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'dynamicdns/dynamicdns.js' %}"
|
||||
defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_content %}
|
||||
<h3>{% trans "Status" %}</h3>
|
||||
<h3>{% trans "Domains" %}</h3>
|
||||
|
||||
<div class="btn-toolbar">
|
||||
<a href="{% url 'dynamicdns:domain-add' %}" class="btn btn-primary"
|
||||
role="button" title="{% trans 'Add Domain' %}">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
{% trans 'Add Domain' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if domains_status %}
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive domains-status">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -24,12 +26,20 @@
|
||||
<th>{% trans "Last update" %}</th>
|
||||
<th>{% trans "Result" %}</th>
|
||||
<th>{% trans "IP Address" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain in domains_status.values %}
|
||||
<tr>
|
||||
<td>{{ domain.domain }}</td>
|
||||
<td class="domain-name">
|
||||
<a href="{% url 'dynamicdns:domain-edit' domain.domain %}"
|
||||
title="{% blocktrans trimmed with domain=domain.domain %}
|
||||
Edit domain {{ domain }}
|
||||
{% endblocktrans %}">
|
||||
{{ domain.domain }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ domain.timestamp|timesince }}</td>
|
||||
<td>
|
||||
{% if domain.result %}
|
||||
@ -48,12 +58,28 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ domain.ip_address|default_if_none:'-' }}</td>
|
||||
<td>
|
||||
<a href="{% url 'dynamicdns:domain-edit' domain.domain %}"
|
||||
class="btn btn-default btn-sm domain-edit" role="button"
|
||||
title="{% blocktrans trimmed with domain=domain.domain %}
|
||||
Edit domain {{ domain }}
|
||||
{% endblocktrans %}">
|
||||
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a href="{% url 'dynamicdns:domain-delete' domain.domain %}"
|
||||
class="btn btn-default btn-sm domain-delete" role="button"
|
||||
title="{% blocktrans trimmed with domain=domain.main %}
|
||||
Delete domain {{ domain }}
|
||||
{% endblocktrans %}">
|
||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
{% trans "No status available." %}
|
||||
<p>{% trans "No domains configured." %}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -76,8 +76,8 @@ class TestDynamicDNSApp(functional.BaseAppTests):
|
||||
@staticmethod
|
||||
def test_capitalized_domain_name(session_browser):
|
||||
"""Test handling of capitalized domain name."""
|
||||
_configure(session_browser, _configs['gnudip1'])
|
||||
_configure(session_browser, {'domain': 'FreedomBox.example.com'})
|
||||
config = dict(_configs['gnudip1'], domain='FreedomBox.example.com')
|
||||
_configure(session_browser, config)
|
||||
_assert_has_config(session_browser,
|
||||
{'domain': 'freedombox.example.com'})
|
||||
|
||||
@ -105,32 +105,54 @@ class TestDynamicDNSApp(functional.BaseAppTests):
|
||||
|
||||
def _configure(browser, config):
|
||||
functional.nav_to_module(browser, 'dynamicdns')
|
||||
current_domains = _get_domains(browser)
|
||||
for domain in current_domains:
|
||||
if domain.endswith('.example.com'):
|
||||
_delete_domain(browser, domain)
|
||||
|
||||
functional.nav_to_module(browser, 'dynamicdns')
|
||||
functional.click_link_by_href(browser,
|
||||
'/plinth/sys/dynamicdns/domain/add/')
|
||||
for key, value in config.items():
|
||||
field_id = f'id_domain-{key}'
|
||||
if key == 'service_type':
|
||||
browser.find_by_id(f'id_{key}').select(value)
|
||||
browser.find_by_id(field_id).select(value)
|
||||
elif isinstance(value, bool):
|
||||
if value:
|
||||
browser.find_by_id(f'id_{key}').check()
|
||||
browser.find_by_id(field_id).check()
|
||||
else:
|
||||
browser.find_by_id(f'id_{key}').uncheck()
|
||||
browser.find_by_id(field_id).uncheck()
|
||||
else:
|
||||
browser.find_by_id(f'id_{key}').fill(value)
|
||||
browser.find_by_id(field_id).fill(value)
|
||||
|
||||
functional.submit(browser, form_class='form-configuration')
|
||||
functional.submit(browser, form_class='form-domain')
|
||||
|
||||
|
||||
def _assert_has_config(browser, config):
|
||||
functional.nav_to_module(browser, 'dynamicdns')
|
||||
link = f'/plinth/sys/dynamicdns/domain/{config["domain"]}/edit/'
|
||||
functional.click_link_by_href(browser, link)
|
||||
for key, value in config.items():
|
||||
if key == 'password':
|
||||
continue
|
||||
|
||||
field_id = f'id_domain-{key}'
|
||||
if isinstance(value, bool):
|
||||
assert browser.find_by_id(f'id_{key}').checked == value
|
||||
assert browser.find_by_id(field_id).checked == value
|
||||
else:
|
||||
assert value == browser.find_by_id(f'id_{key}').value
|
||||
assert value == browser.find_by_id(field_id).value
|
||||
|
||||
|
||||
def _get_domain(browser):
|
||||
def _get_domains(browser):
|
||||
"""Return the list of configured domains."""
|
||||
functional.nav_to_module(browser, 'dynamicdns')
|
||||
return browser.find_by_id('id_domain').value
|
||||
elements = browser.find_by_css('.domains-status .domain-name a')
|
||||
return [element.text.strip() for element in elements]
|
||||
|
||||
|
||||
def _delete_domain(browser, domain):
|
||||
"""Delete a given domain."""
|
||||
functional.nav_to_module(browser, 'dynamicdns')
|
||||
link = f'/plinth/sys/dynamicdns/domain/{domain}/delete/'
|
||||
functional.click_link_by_href(browser, link)
|
||||
functional.submit(browser, form_class='form-delete')
|
||||
|
||||
@ -10,4 +10,10 @@ from . import views
|
||||
urlpatterns = [
|
||||
re_path(r'^sys/dynamicdns/$', views.DynamicDNSAppView.as_view(),
|
||||
name='index'),
|
||||
re_path(r'^sys/dynamicdns/domain/add/$', views.DomainView.as_view(),
|
||||
name='domain-add'),
|
||||
re_path(r'^sys/dynamicdns/domain/(?P<domain>[^/]+)/edit/$',
|
||||
views.DomainView.as_view(), name='domain-edit'),
|
||||
re_path(r'^sys/dynamicdns/domain/(?P<domain>[^/]+)/delete/$',
|
||||
views.DomainDeleteView.as_view(), name='domain-delete'),
|
||||
]
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Views for the dynamicsdns module.
|
||||
"""
|
||||
"""Views for the dynamicsdns module."""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from plinth import views
|
||||
from plinth.modules import dynamicdns
|
||||
|
||||
from .forms import ConfigureForm
|
||||
from .forms import DomainForm
|
||||
|
||||
|
||||
class DynamicDNSAppView(views.AppView):
|
||||
"""Serve configuration page."""
|
||||
"""View to show app status."""
|
||||
|
||||
app_id = 'dynamicdns'
|
||||
template_name = 'dynamicdns.html'
|
||||
form_class = ConfigureForm
|
||||
|
||||
_error_messages = {
|
||||
'timeout': _('Connection timed out'),
|
||||
@ -31,8 +33,10 @@ class DynamicDNSAppView(views.AppView):
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Return the context data for rendering the template view."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
status = dynamicdns.get_status()
|
||||
config = dynamicdns.get_config()
|
||||
context['domains'] = config['domains']
|
||||
|
||||
status = dynamicdns.get_status()
|
||||
domains_status = {}
|
||||
for domain_name, domain in status['domains'].items():
|
||||
if domain_name not in config['domains']:
|
||||
@ -49,11 +53,35 @@ class DynamicDNSAppView(views.AppView):
|
||||
context['domains_status'] = domains_status
|
||||
return context
|
||||
|
||||
|
||||
class DomainView(FormView):
|
||||
"""View to add/edit a dynamic DNS domain."""
|
||||
|
||||
template_name = 'dynamicdns-domain.html'
|
||||
form_class = DomainForm
|
||||
prefix = 'domain'
|
||||
success_url = reverse_lazy('dynamicdns:index')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Return the context data for rendering the template view."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
domain_name = self.kwargs.get('domain')
|
||||
if not domain_name:
|
||||
context['title'] = _('Add Dynamic Domain')
|
||||
else:
|
||||
context['title'] = _('Edit Dynamic Domain')
|
||||
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
"""Get the current values for the form."""
|
||||
initial = super().get_initial()
|
||||
domain_name = self.kwargs.get('domain')
|
||||
domains = dynamicdns.get_config()['domains']
|
||||
domain = list(domains.values())[0] if domains else {}
|
||||
domain = {}
|
||||
if domains and domain_name and domain_name in domains:
|
||||
domain = domains[domain_name]
|
||||
|
||||
initial.update(domain)
|
||||
return domain
|
||||
|
||||
@ -63,21 +91,59 @@ class DynamicDNSAppView(views.AppView):
|
||||
new_status = form.cleaned_data
|
||||
|
||||
if old_status != new_status:
|
||||
config = dynamicdns.get_config()
|
||||
try:
|
||||
del config['domains'][old_status['domain']]
|
||||
_domain_delete(old_status['domain'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
config['domains'][new_status['domain']] = new_status
|
||||
dynamicdns.set_config(config)
|
||||
if old_status.get('domain'):
|
||||
dynamicdns.notify_domain_removed(old_status['domain'])
|
||||
|
||||
dynamicdns.notify_domain_added(new_status['domain'])
|
||||
_domain_add(new_status['domain'], new_status)
|
||||
messages.success(self.request, _('Configuration updated'))
|
||||
|
||||
# Perform an immediate update, even when configuration is not changed.
|
||||
dynamicdns.update_dns(None)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
def _domain_add(domain: str, domain_config: dict):
|
||||
"""Add a domain to the configuration."""
|
||||
config = dynamicdns.get_config()
|
||||
config['domains'][domain] = domain_config
|
||||
dynamicdns.set_config(config)
|
||||
dynamicdns.notify_domain_added(domain)
|
||||
|
||||
|
||||
def _domain_delete(domain: str):
|
||||
"""Remove a domain from the configuration.
|
||||
|
||||
Raises KeyError if the domain is not found in the configuration.
|
||||
"""
|
||||
config = dynamicdns.get_config()
|
||||
del config['domains'][domain]
|
||||
dynamicdns.set_config(config)
|
||||
if domain:
|
||||
dynamicdns.notify_domain_removed(domain)
|
||||
|
||||
|
||||
class DomainDeleteView(TemplateView):
|
||||
"""Confirm and delete a domain."""
|
||||
template_name = 'dynamicdns-domain-delete.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Return additional context data for rendering the template."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
domain = self.kwargs['domain']
|
||||
context['domain'] = domain
|
||||
context['title'] = str(
|
||||
_('Delete Domain {domain}?')).format(domain=domain)
|
||||
return context
|
||||
|
||||
def post(self, request, domain):
|
||||
"""Delete a domain."""
|
||||
try:
|
||||
_domain_delete(domain)
|
||||
messages.success(request, _('Domain deleted.'))
|
||||
except KeyError:
|
||||
raise
|
||||
|
||||
return redirect('dynamicdns:index')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user