Add continuous collection reading view

This commit is contained in:
rucki
2026-08-27 16:14:10 +02:00
parent cd97a6cb62
commit 31d1828adb
7 changed files with 143 additions and 69 deletions
File diff suppressed because one or more lines are too long
+19 -48
View File
@@ -2,66 +2,37 @@
{% load i18n markdown_extras %} {% load i18n markdown_extras %}
{% block content %} {% block content %}
<div class="d-md-none alert alert-info">{% trans "Collections are currently available on desktop only." %}</div> <div class="d-md-none alert alert-info">{% trans "Collections are currently available on desktop only." %}</div>
<div class="d-none d-md-block"> <article class="d-none d-md-block collection-document">
<div class="d-flex justify-content-between align-items-start gap-3 mb-4"> <header class="mb-5">
<div class="d-flex justify-content-between align-items-start gap-3">
<div> <div>
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% url 'collection_list' %}">← {% trans "Collections" %}</a> <a class="btn btn-sm btn-outline-secondary mb-3" href="{% if user.is_authenticated %}{% url 'collection_list' %}{% else %}/{% endif %}">← {% if user.is_authenticated %}{% trans "Collections" %}{% else %}Start{% endif %}</a>
<h1 class="display-6 mb-2">{{ collection.title }}</h1> <h1 class="display-5 mb-3">{{ collection.title }}</h1>
<div class="d-flex flex-wrap gap-2 align-items-center"> <div class="d-flex flex-wrap gap-2 align-items-center">
{% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %} {% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %}
{% for tag in collection.tags.all %}<span class="badge rounded-pill text-bg-light">#{{ tag.name }}</span>{% endfor %} {% for tag in collection.tags.all %}<span class="badge rounded-pill text-bg-light">#{{ tag.name }}</span>{% endfor %}
</div> </div>
</div> </div>
{% if can_edit %}<div class="d-flex gap-2"><a class="btn btn-outline-secondary" href="{% url 'collection_edit' collection.id %}">{% trans "Edit collection" %}</a>{% if can_delete %}<form method="post" action="{% url 'collection_delete' collection.id %}" onsubmit="return confirm('{% trans "Delete collection? The notes remain available." %}')">{% csrf_token %}<button class="btn btn-outline-danger">{% trans "Delete" %}</button></form>{% endif %}</div>{% endif %} {% if can_edit %}<a class="btn btn-dark" href="{% url 'collection_manage' collection.id %}">{% trans "Edit collection" %}</a>{% endif %}
</div> </div>
{% if collection.description %}<div class="content lead mt-4">{{ collection.description|markdown }}</div>{% endif %}
{% if collection.description %}<div class="content lead mb-4">{{ collection.description|markdown }}</div>{% endif %} </header>
{% if sections %} {% if sections %}
<div class="card item-card mb-4"><div class="card-body"><div class="fw-semibold mb-2">{% trans "Contents" %}</div><ol class="mb-0">{% for section in sections %}<li><a href="#section-{{ section.id }}">{{ section.title|default:section.item.content|truncatechars:90 }}</a></li>{% endfor %}</ol></div></div> <nav class="collection-toc mb-5" aria-label="{% trans 'Contents' %}">
<div class="small fw-semibold text-uppercase text-muted mb-2">{% trans "Contents" %}</div>
<ol class="mb-0">{% for section in sections %}<li><a href="#section-{{ section.id }}">{{ section.title|default:section.item.content|truncatechars:90 }}</a></li>{% endfor %}</ol>
</nav>
{% endif %} {% endif %}
{% if pending_item %} <div class="collection-flow content">
<div class="alert alert-warning">
<h2 class="h5">{% trans "The note has a different visibility" %}</h2>
<p>{% trans "To add it, change its visibility or create a separate copy for this collection." %}</p>
<div class="border rounded bg-white p-2 mb-3">{{ pending_item.content|truncatechars:180 }}</div>
<div class="d-flex gap-2">
{% if pending_can_convert %}<form method="post">{% csrf_token %}<input type="hidden" name="action" value="confirm_add"><input type="hidden" name="item_id" value="{{ pending_item.id }}"><input type="hidden" name="mode" value="convert"><button class="btn btn-warning">{% if collection.visibility == 'public' %}{% trans "Make note public" %}{% elif collection.team %}{% trans "Share note with team" %}{% else %}{% trans "Make note private" %}{% endif %}</button></form>{% endif %}
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="confirm_add"><input type="hidden" name="item_id" value="{{ pending_item.id }}"><input type="hidden" name="mode" value="copy"><button class="btn btn-outline-secondary">{% trans "Create compatible copy" %}</button></form>
<a class="btn btn-link" href="{{ collection.get_absolute_url }}">{% trans "Cancel" %}</a>
</div>
{% if not pending_can_convert %}<div class="small text-muted mt-2">{% trans "The original cannot be changed because it is used elsewhere or you cannot edit it." %}</div>{% endif %}
</div>
{% endif %}
<div class="vstack gap-4 mb-5">
{% for section in sections %} {% for section in sections %}
<section id="section-{{ section.id }}" class="card item-card"><div class="card-body"> <section id="section-{{ section.id }}" class="collection-flow-section">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3"> {% if section.title %}<h2>{{ section.title }}</h2>{% endif %}
<h2 class="h3 mb-0">{{ section.title|default:section.item.content|truncatechars:100 }}</h2> {{ section.item|item_markdown }}
<a class="small text-muted" href="{{ section.item.get_absolute_url }}">#{{ section.item.id }}</a> {% if section.item.comment %}<div class="text-muted border-start ps-3 mt-3">{{ section.item.comment|markdown }}</div>{% endif %}
</div> </section>
<div class="content">{{ section.item|item_markdown }}</div>
{% if section.item.comment %}<div class="content text-muted border-start ps-3 mt-3">{{ section.item.comment|markdown }}</div>{% endif %}
{% if can_edit %}<div class="border-top mt-4 pt-3 d-flex align-items-center gap-2">
<form method="post" class="d-flex gap-2 flex-grow-1">{% csrf_token %}<input type="hidden" name="action" value="title"><input type="hidden" name="section_id" value="{{ section.id }}"><input class="form-control form-control-sm" name="title" value="{{ section.title }}" placeholder="{% trans 'Optional section title' %}"><button class="btn btn-sm btn-outline-secondary">{% trans "Save title" %}</button></form>
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="up"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-secondary" title="{% trans 'Move up' %}">↑</button></form>
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="down"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-secondary" title="{% trans 'Move down' %}">↓</button></form>
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="remove"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-danger">{% trans "Remove" %}</button></form>
</div>{% endif %}
</div></section>
{% empty %}<div class="text-center text-muted py-5">{% trans "This collection has no sections yet." %}</div>{% endfor %} {% empty %}<div class="text-center text-muted py-5">{% trans "This collection has no sections yet." %}</div>{% endfor %}
</div> </div>
</article>
{% if can_edit %}
<div class="card item-card"><div class="card-body">
<h2 class="h4">{% trans "Add existing note" %}</h2>
<form method="get" class="input-group mb-3"><input class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search notes' %}"><button class="btn btn-outline-secondary">{% trans "Search" %}</button></form>
<div class="vstack gap-2">
{% for item in candidates %}<div class="border rounded p-3 d-flex justify-content-between align-items-start gap-3"><div><div>{{ item.content|truncatechars:180 }}</div><div class="small text-muted">#{{ item.id }} · {{ item.get_visibility_display }}{% if item.team %} · {{ item.team.name }}{% endif %}</div></div><form method="post">{% csrf_token %}<input type="hidden" name="action" value="add"><input type="hidden" name="item_id" value="{{ item.id }}"><button class="btn btn-sm btn-dark">{% trans "Add" %}</button></form></div>{% empty %}<div class="text-muted">{% trans "No matching notes." %}</div>{% endfor %}
</div>
</div></div>
{% endif %}
</div>
{% endblock %} {% endblock %}
@@ -0,0 +1,67 @@
{% extends 'core/base.html' %}
{% load i18n markdown_extras %}
{% block content %}
<div class="d-md-none alert alert-info">{% trans "Collections are currently available on desktop only." %}</div>
<div class="d-none d-md-block">
<div class="d-flex justify-content-between align-items-start gap-3 mb-4">
<div>
<a class="btn btn-sm btn-outline-secondary mb-3" href="{{ collection.get_absolute_url }}">← {% trans "View collection" %}</a>
<h1 class="display-6 mb-2">{{ collection.title }}</h1>
<div class="d-flex flex-wrap gap-2 align-items-center">
{% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %}
{% for tag in collection.tags.all %}<span class="badge rounded-pill text-bg-light">#{{ tag.name }}</span>{% endfor %}
</div>
</div>
{% if can_edit %}<div class="d-flex gap-2"><a class="btn btn-outline-secondary" href="{% url 'collection_edit' collection.id %}">{% trans "Collection settings" %}</a>{% if can_delete %}<form method="post" action="{% url 'collection_delete' collection.id %}" onsubmit="return confirm('{% trans "Delete collection? The notes remain available." %}')">{% csrf_token %}<button class="btn btn-outline-danger">{% trans "Delete" %}</button></form>{% endif %}</div>{% endif %}
</div>
{% if collection.description %}<div class="content lead mb-4">{{ collection.description|markdown }}</div>{% endif %}
{% if sections %}
<div class="card item-card mb-4"><div class="card-body"><div class="fw-semibold mb-2">{% trans "Contents" %}</div><ol class="mb-0">{% for section in sections %}<li><a href="#section-{{ section.id }}">{{ section.title|default:section.item.content|truncatechars:90 }}</a></li>{% endfor %}</ol></div></div>
{% endif %}
{% if pending_item %}
<div class="alert alert-warning">
<h2 class="h5">{% trans "The note has a different visibility" %}</h2>
<p>{% trans "To add it, change its visibility or create a separate copy for this collection." %}</p>
<div class="border rounded bg-white p-2 mb-3">{{ pending_item.content|truncatechars:180 }}</div>
<div class="d-flex gap-2">
{% if pending_can_convert %}<form method="post">{% csrf_token %}<input type="hidden" name="action" value="confirm_add"><input type="hidden" name="item_id" value="{{ pending_item.id }}"><input type="hidden" name="mode" value="convert"><button class="btn btn-warning">{% if collection.visibility == 'public' %}{% trans "Make note public" %}{% elif collection.team %}{% trans "Share note with team" %}{% else %}{% trans "Make note private" %}{% endif %}</button></form>{% endif %}
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="confirm_add"><input type="hidden" name="item_id" value="{{ pending_item.id }}"><input type="hidden" name="mode" value="copy"><button class="btn btn-outline-secondary">{% trans "Create compatible copy" %}</button></form>
<a class="btn btn-link" href="{% url 'collection_manage' collection.id %}">{% trans "Cancel" %}</a>
</div>
{% if not pending_can_convert %}<div class="small text-muted mt-2">{% trans "The original cannot be changed because it is used elsewhere or you cannot edit it." %}</div>{% endif %}
</div>
{% endif %}
<div class="vstack gap-4 mb-5">
{% for section in sections %}
<section id="section-{{ section.id }}" class="card item-card"><div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<h2 class="h3 mb-0">{{ section.title|default:section.item.content|truncatechars:100 }}</h2>
<a class="small text-muted" href="{{ section.item.get_absolute_url }}">#{{ section.item.id }}</a>
</div>
<div class="content">{{ section.item|item_markdown }}</div>
{% if section.item.comment %}<div class="content text-muted border-start ps-3 mt-3">{{ section.item.comment|markdown }}</div>{% endif %}
{% if can_edit %}<div class="border-top mt-4 pt-3 d-flex align-items-center gap-2">
<form method="post" class="d-flex gap-2 flex-grow-1">{% csrf_token %}<input type="hidden" name="action" value="title"><input type="hidden" name="section_id" value="{{ section.id }}"><input class="form-control form-control-sm" name="title" value="{{ section.title }}" placeholder="{% trans 'Optional section title' %}"><button class="btn btn-sm btn-outline-secondary">{% trans "Save title" %}</button></form>
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="up"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-secondary" title="{% trans 'Move up' %}">↑</button></form>
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="down"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-secondary" title="{% trans 'Move down' %}">↓</button></form>
<form method="post">{% csrf_token %}<input type="hidden" name="action" value="remove"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-danger">{% trans "Remove" %}</button></form>
</div>{% endif %}
</div></section>
{% empty %}<div class="text-center text-muted py-5">{% trans "This collection has no sections yet." %}</div>{% endfor %}
</div>
{% if can_edit %}
<div class="card item-card"><div class="card-body">
<h2 class="h4">{% trans "Add existing note" %}</h2>
<form method="get" class="input-group mb-3"><input class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search notes' %}"><button class="btn btn-outline-secondary">{% trans "Search" %}</button></form>
<div class="vstack gap-2">
{% for item in candidates %}<div class="border rounded p-3 d-flex justify-content-between align-items-start gap-3"><div><div>{{ item.content|truncatechars:180 }}</div><div class="small text-muted">#{{ item.id }} · {{ item.get_visibility_display }}{% if item.team %} · {{ item.team.name }}{% endif %}</div></div><form method="post">{% csrf_token %}<input type="hidden" name="action" value="add"><input type="hidden" name="item_id" value="{{ item.id }}"><button class="btn btn-sm btn-dark">{% trans "Add" %}</button></form></div>{% empty %}<div class="text-muted">{% trans "No matching notes." %}</div>{% endfor %}
</div>
</div></div>
{% endif %}
</div>
{% endblock %}
+22 -6
View File
@@ -159,20 +159,22 @@ class CollectionTests(TestCase):
collection = Collection.objects.get() collection = Collection.objects.get()
self.assertRedirects(response, collection.get_absolute_url()) self.assertRedirects(response, collection.get_absolute_url())
note = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Introduction') note = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Introduction')
response = self.client.post(collection.get_absolute_url(), {'action': 'add', 'item_id': note.id}) manage_url = reverse('collection_manage', args=[collection.id])
self.assertRedirects(response, collection.get_absolute_url()) response = self.client.post(manage_url, {'action': 'add', 'item_id': note.id})
self.assertRedirects(response, manage_url)
self.assertTrue(CollectionSection.objects.filter(collection=collection, item=note).exists()) self.assertTrue(CollectionSection.objects.filter(collection=collection, item=note).exists())
def test_public_collection_asks_before_copying_private_note(self): def test_public_collection_asks_before_copying_private_note(self):
collection = Collection.objects.create(owner=self.user, title='Public guide', visibility=Item.Visibility.PUBLIC) collection = Collection.objects.create(owner=self.user, title='Public guide', visibility=Item.Visibility.PUBLIC)
note = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Private section') note = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Private section')
response = self.client.post(collection.get_absolute_url(), {'action': 'add', 'item_id': note.id}) manage_url = reverse('collection_manage', args=[collection.id])
response = self.client.post(manage_url, {'action': 'add', 'item_id': note.id})
self.assertContains(response, 'name="mode" value="copy"') self.assertContains(response, 'name="mode" value="copy"')
response = self.client.post(collection.get_absolute_url(), { response = self.client.post(manage_url, {
'action': 'confirm_add', 'item_id': note.id, 'mode': 'copy', 'action': 'confirm_add', 'item_id': note.id, 'mode': 'copy',
}) })
self.assertRedirects(response, collection.get_absolute_url()) self.assertRedirects(response, manage_url)
copied = collection.sections.get().item copied = collection.sections.get().item
self.assertNotEqual(copied.id, note.id) self.assertNotEqual(copied.id, note.id)
self.assertEqual(copied.visibility, Item.Visibility.PUBLIC) self.assertEqual(copied.visibility, Item.Visibility.PUBLIC)
@@ -199,10 +201,24 @@ class CollectionTests(TestCase):
self.assertEqual(collection.team, team) self.assertEqual(collection.team, team)
self.assertEqual(collection.visibility, Item.Visibility.TEAM) self.assertEqual(collection.visibility, Item.Visibility.TEAM)
def test_collection_opens_as_continuous_reading_view(self):
collection = Collection.objects.create(owner=self.user, title='Reading view')
first = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='First paragraph')
second = Item.objects.create(owner=self.user, kind=Item.Kind.NOTE, content='Second paragraph')
CollectionSection.objects.create(collection=collection, item=first, title='First', position=0)
CollectionSection.objects.create(collection=collection, item=second, position=1)
response = self.client.get(collection.get_absolute_url())
self.assertContains(response, 'collection-flow')
self.assertContains(response, reverse('collection_manage', args=[collection.id]))
self.assertNotContains(response, 'Add existing note')
self.assertNotContains(response, 'section_id')
def test_public_collection_is_visible_without_login(self): def test_public_collection_is_visible_without_login(self):
collection = Collection.objects.create(owner=self.user, title='Public', visibility=Item.Visibility.PUBLIC) collection = Collection.objects.create(owner=self.user, title='Public', visibility=Item.Visibility.PUBLIC)
self.client.logout() self.client.logout()
self.assertEqual(self.client.get(collection.get_absolute_url()).status_code, 200) response = self.client.get(collection.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, reverse('collection_manage', args=[collection.id]))
def test_collection_tag_is_not_removed_as_unused(self): def test_collection_tag_is_not_removed_as_unused(self):
tag = Tag.objects.create(name='documentation') tag = Tag.objects.create(name='documentation')
+1
View File
@@ -22,6 +22,7 @@ urlpatterns = [
path('collections/', views.collection_list, name='collection_list'), path('collections/', views.collection_list, name='collection_list'),
path('collections/new/', views.collection_create, name='collection_create'), path('collections/new/', views.collection_create, name='collection_create'),
path('collections/<int:pk>/', views.collection_detail, name='collection_detail'), path('collections/<int:pk>/', views.collection_detail, name='collection_detail'),
path('collections/<int:pk>/manage/', views.collection_manage, name='collection_manage'),
path('collections/<int:pk>/edit/', views.collection_edit, name='collection_edit'), path('collections/<int:pk>/edit/', views.collection_edit, name='collection_edit'),
path('collections/<int:pk>/delete/', views.collection_delete, name='collection_delete'), path('collections/<int:pk>/delete/', views.collection_delete, name='collection_delete'),
path('items/<int:pk>/', views.item_detail, name='item_detail'), path('items/<int:pk>/', views.item_detail, name='item_detail'),
+19 -8
View File
@@ -692,29 +692,40 @@ def collection_edit(request, pk):
def collection_detail(request, pk): def collection_detail(request, pk):
collection = get_object_or_404(visible_collections(request.user), pk=pk) collection = get_object_or_404(visible_collections(request.user), pk=pk)
if request.method == 'POST': return render(request, 'core/collection_detail.html', {
'collection': collection,
'sections': collection.sections.select_related('item').prefetch_related('item__tags', 'item__attachments'),
'can_edit': user_can_edit_collection(request.user, collection),
})
@login_required
def collection_manage(request, pk):
collection = get_object_or_404(visible_collections(request.user), pk=pk)
if not user_can_edit_collection(request.user, collection): if not user_can_edit_collection(request.user, collection):
return JsonResponse({'error': 'forbidden'}, status=403) return JsonResponse({'error': 'forbidden'}, status=403)
manage_url = reverse('collection_manage', args=[collection.pk])
if request.method == 'POST':
action = request.POST.get('action') action = request.POST.get('action')
if action in {'add', 'confirm_add'}: if action in {'add', 'confirm_add'}:
item = get_object_or_404(visible_items(request.user), pk=request.POST.get('item_id'), kind=Item.Kind.NOTE) item = get_object_or_404(visible_items(request.user), pk=request.POST.get('item_id'), kind=Item.Kind.NOTE)
if collection_item_is_compatible(collection, item): if collection_item_is_compatible(collection, item):
add_collection_section(collection, item) add_collection_section(collection, item)
return redirect(collection) return redirect(manage_url)
if action == 'add': if action == 'add':
return render(request, 'core/collection_detail.html', collection_detail_context(request, collection, item)) return render(request, 'core/collection_manage.html', collection_detail_context(request, collection, item))
mode = request.POST.get('mode') mode = request.POST.get('mode')
if mode == 'convert': if mode == 'convert':
if not item_can_change_for_collection(request.user, item, collection): if not item_can_change_for_collection(request.user, item, collection):
messages.error(request, _('This note is used by an incompatible collection or cannot be edited. Create a copy instead.')) messages.error(request, _('This note is used by an incompatible collection or cannot be edited. Create a copy instead.'))
return redirect(collection) return redirect(manage_url)
item = adapt_item_for_collection(item, collection) item = adapt_item_for_collection(item, collection)
elif mode == 'copy': elif mode == 'copy':
item = copy_item_for_collection(item, collection, request.user) item = copy_item_for_collection(item, collection, request.user)
else: else:
return redirect(collection) return redirect(manage_url)
add_collection_section(collection, item) add_collection_section(collection, item)
return redirect(collection) return redirect(manage_url)
section = get_object_or_404(CollectionSection, pk=request.POST.get('section_id'), collection=collection) section = get_object_or_404(CollectionSection, pk=request.POST.get('section_id'), collection=collection)
if action == 'remove': if action == 'remove':
section.delete() section.delete()
@@ -733,8 +744,8 @@ def collection_detail(request, pk):
CollectionSection.objects.filter(pk=section.pk).update(position=temporary) CollectionSection.objects.filter(pk=section.pk).update(position=temporary)
CollectionSection.objects.filter(pk=other.pk).update(position=section.position) CollectionSection.objects.filter(pk=other.pk).update(position=section.position)
CollectionSection.objects.filter(pk=section.pk).update(position=other.position) CollectionSection.objects.filter(pk=section.pk).update(position=other.position)
return redirect(collection) return redirect(manage_url)
return render(request, 'core/collection_detail.html', collection_detail_context(request, collection)) return render(request, 'core/collection_manage.html', collection_detail_context(request, collection))
@login_required @login_required
+8
View File
@@ -664,6 +664,14 @@ msgstr "Sammlungen sind derzeit nur auf dem Desktop verfügbar."
msgid "Edit collection" msgid "Edit collection"
msgstr "Sammlung bearbeiten" msgstr "Sammlung bearbeiten"
#: core/templates/core/collection_manage.html
msgid "View collection"
msgstr "Sammlung ansehen"
#: core/templates/core/collection_manage.html
msgid "Collection settings"
msgstr "Sammlungseinstellungen"
#: core/templates/core/collection_detail.html #: core/templates/core/collection_detail.html
msgid "Delete collection? The notes remain available." msgid "Delete collection? The notes remain available."
msgstr "Sammlung löschen? Die Notizen bleiben erhalten." msgstr "Sammlung löschen? Die Notizen bleiben erhalten."