Einstellungen: Cloud-Verbindung bearbeiten (Name, URL, Benutzer, Unterordner, Passwort); sw v99
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 7m52s

Bisher ließ sich eine Verbindung nur anlegen und löschen, der Startordner
also nicht nachträglich setzen. Das Passwort wird nicht angezeigt; leer
lassen behält das gespeicherte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-08 19:22:04 +02:00
parent 53ca3b0002
commit fda6c01943
6 changed files with 115 additions and 1 deletions

View file

@ -546,3 +546,51 @@ class CloudFetchCachingTests(TestCase):
with patch('books.webdav.fetch_file', return_value=b'EPUB-BYTES'): with patch('books.webdav.fetch_file', return_value=b'EPUB-BYTES'):
resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=Dune.epub') resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=Dune.epub')
self.assertEqual(resp['Cache-Control'], 'no-store') self.assertEqual(resp['Cache-Control'], 'no-store')
class WebDAVEditTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='alice', password='pw12345678')
self.other = User.objects.create_user(username='bob', password='pw12345678')
self.source = WebDAVSource.objects.create(
user=self.user, label='cloud', base_url='https://cloud.example.com',
username='alice', password='app-pw',
)
def _edit(self, pk, **data):
form = {'label': 'cloud', 'base_url': 'https://cloud.example.com', 'username': 'alice', 'root_path': ''}
form.update(data)
with patch('accounts.views.list_directory', return_value=[]):
return self.client.post(f'/accounts/webdav/{pk}/edit/', form)
def test_sets_start_folder_and_keeps_password(self):
self.client.force_login(self.user)
resp = self._edit(self.source.pk, root_path='Books', password='')
self.assertEqual(resp.status_code, 302)
self.source.refresh_from_db()
self.assertEqual(self.source.root_path, 'Books')
self.assertEqual(self.source.password, 'app-pw')
def test_new_password_replaces_the_old_one(self):
self.client.force_login(self.user)
self._edit(self.source.pk, password='new-pw')
self.source.refresh_from_db()
self.assertEqual(self.source.password, 'new-pw')
def test_name_and_url_are_required(self):
self.client.force_login(self.user)
self._edit(self.source.pk, label='', root_path='Books')
self.source.refresh_from_db()
self.assertEqual(self.source.root_path, '')
def test_foreign_source_is_not_editable(self):
self.client.force_login(self.other)
self._edit(self.source.pk, root_path='x')
self.source.refresh_from_db()
self.assertEqual(self.source.root_path, '')
def test_settings_page_does_not_show_the_password(self):
self.client.force_login(self.user)
html = self.client.get('/accounts/settings/').content.decode()
self.assertIn(f'/accounts/webdav/{self.source.pk}/edit/', html)
self.assertNotIn('app-pw', html)

View file

@ -17,6 +17,7 @@ urlpatterns = [
path('change-password/', views.change_password, name='change_password'), path('change-password/', views.change_password, name='change_password'),
path('api-token/regenerate/', views.regenerate_api_token, name='regenerate_api_token'), path('api-token/regenerate/', views.regenerate_api_token, name='regenerate_api_token'),
path('webdav/add/', views.webdav_add, name='webdav_add'), path('webdav/add/', views.webdav_add, name='webdav_add'),
path('webdav/<int:pk>/edit/', views.webdav_edit, name='webdav_edit'),
path('webdav/<int:pk>/test/', views.webdav_test, name='webdav_test'), path('webdav/<int:pk>/test/', views.webdav_test, name='webdav_test'),
path('webdav/<int:pk>/delete/', views.webdav_delete, name='webdav_delete'), path('webdav/<int:pk>/delete/', views.webdav_delete, name='webdav_delete'),
] ]

View file

@ -290,6 +290,35 @@ def webdav_add(request):
return redirect('settings') return redirect('settings')
@login_required
@require_http_methods(['POST'])
def webdav_edit(request, pk):
source = WebDAVSource.objects.filter(pk=pk, user=request.user).first()
if not source:
messages.error(request, 'Verbindung nicht gefunden.')
return redirect('settings')
label = request.POST.get('label', '').strip()
base_url = request.POST.get('base_url', '').strip()
if not label or not base_url:
messages.error(request, 'Name und Server-URL sind erforderlich.')
return redirect('settings')
source.label = label[:100]
source.base_url = base_url[:500]
source.username = request.POST.get('username', '').strip()[:200]
source.root_path = request.POST.get('root_path', '').strip()[:500]
# The stored password is never sent to the page: empty means keep it.
password = request.POST.get('password', '')
if password:
source.password = password[:500]
source.save()
level, message = _probe_source(source)
messages.add_message(request, level, message)
return redirect('settings')
@login_required @login_required
@require_http_methods(['POST']) @require_http_methods(['POST'])
def webdav_test(request, pk): def webdav_test(request, pk):

View file

@ -826,6 +826,15 @@ a:hover {
align-self: flex-start; align-self: flex-start;
} }
/* Editing a saved connection: a full row under its name and buttons */
.webdav-edit {
flex-basis: 100%;
padding-top: 0.75rem;
}
.webdav-edit[hidden] {
display: none;
}
/* --- Cloud import browser (books tab) --- */ /* --- Cloud import browser (books tab) --- */
.cloud-browser { .cloud-browser {

View file

@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use. * diora service worker — caches the app shell for offline use.
*/ */
const CACHE = 'diora-v98'; const CACHE = 'diora-v99';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',
'/static/js/app.js', '/static/js/app.js',

View file

@ -150,6 +150,8 @@
<span class="muted">{{ source.base_url }}{% if source.root_path %} · /{{ source.root_path }}{% endif %}</span> <span class="muted">{{ source.base_url }}{% if source.root_path %} · /{{ source.root_path }}{% endif %}</span>
</div> </div>
<div class="webdav-item-actions"> <div class="webdav-item-actions">
<button type="button" class="btn" aria-expanded="false"
onclick="const f = document.getElementById('webdav-edit-{{ source.pk }}'); f.hidden = !f.hidden; this.setAttribute('aria-expanded', String(!f.hidden));">Bearbeiten</button>
<form method="post" action="{% url 'webdav_test' source.pk %}" class="inline-form"> <form method="post" action="{% url 'webdav_test' source.pk %}" class="inline-form">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn">Testen</button> <button type="submit" class="btn">Testen</button>
@ -160,6 +162,31 @@
<button type="submit" class="btn btn-danger">Entfernen</button> <button type="submit" class="btn btn-danger">Entfernen</button>
</form> </form>
</div> </div>
<form method="post" action="{% url 'webdav_edit' source.pk %}" class="webdav-form webdav-edit"
id="webdav-edit-{{ source.pk }}" hidden>
{% csrf_token %}
<label class="webdav-field">
<span>Name</span>
<input type="text" name="label" required value="{{ source.label }}">
</label>
<label class="webdav-field">
<span>Server-URL</span>
<input type="url" name="base_url" required value="{{ source.base_url }}">
</label>
<label class="webdav-field">
<span>Benutzername</span>
<input type="text" name="username" autocomplete="off" value="{{ source.username }}">
</label>
<label class="webdav-field">
<span>App-Passwort <span class="muted">(leer lassen = unverändert)</span></span>
<input type="password" name="password" autocomplete="new-password">
</label>
<label class="webdav-field">
<span>Unterordner <span class="muted">(optional, Import startet dort)</span></span>
<input type="text" name="root_path" value="{{ source.root_path }}" placeholder="Buecher">
</label>
<button type="submit" class="btn">Speichern</button>
</form>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>