Preserve team visibility when editing items
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# my2dos – Projektkontext für Pi
|
||||
|
||||
## Zweck
|
||||
my2dos ist eine kleine persönliche Produktivitäts- und Wissens-App für Aufgaben, Notizen, Links, Journal-Einträge, Tags, Teams, Collections und Kanban.
|
||||
|
||||
## Tech Stack
|
||||
- Python / Django
|
||||
- Django Templates
|
||||
- HTMX für partielle Updates
|
||||
- Alpine.js für UI-Interaktionen im Browser
|
||||
- SQLite lokal (`db.sqlite3`)
|
||||
- Virtuelle Umgebung: `.venv/`
|
||||
|
||||
## Wichtige Pfade
|
||||
- `core/models.py` – Datenmodelle, Tag-/Link-Metadaten-Sync
|
||||
- `core/views.py` – Hauptlogik, Item-Erstellung/Bearbeitung, Teams, Collections, API
|
||||
- `core/forms.py` – Django Forms
|
||||
- `core/templates/core/` – UI Templates
|
||||
- `core/tests.py` – Tests
|
||||
- `my2dos/settings.py` – Django Settings
|
||||
- `README.md` – Setup und Basis-Dokumentation
|
||||
|
||||
## Zentrale Konzepte
|
||||
- `Item.kind`: `todo`, `note`, `link`, `journal`
|
||||
- `Item.visibility`: `private`, `team`, `public`
|
||||
- Tags werden aus `#tag` im Content extrahiert und mit `Item.sync_metadata()` synchronisiert.
|
||||
- Team-Tags entsprechen `Team.slug`.
|
||||
- Ein Team-Tag wie `#myteam` macht ein Item automatisch zum Team-Item.
|
||||
- Team-Items behalten die Verknüpfung über `Item.team` und `Item.visibility = team`.
|
||||
- Interne Links haben die Form `[[note:1]]`, `[[todo:2]]` oder `[[3]]`.
|
||||
- Collections können manuell oder tag-basiert sein.
|
||||
- Tag-basierte Collections verwenden Content-Tags, Team-Tags sollen dabei nicht als Content-Filter genutzt werden.
|
||||
|
||||
## Häufige Befehle
|
||||
Setup:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
.venv/bin/python manage.py migrate
|
||||
.venv/bin/python manage.py createsuperuser
|
||||
```
|
||||
|
||||
Server starten:
|
||||
|
||||
```bash
|
||||
.venv/bin/python manage.py runserver
|
||||
```
|
||||
|
||||
Tests ausführen:
|
||||
|
||||
```bash
|
||||
.venv/bin/python manage.py test core
|
||||
```
|
||||
|
||||
Einzelne Testklasse ausführen:
|
||||
|
||||
```bash
|
||||
.venv/bin/python manage.py test core.tests.TestKlasse
|
||||
```
|
||||
|
||||
## Arbeitsregeln für Coding-Agenten
|
||||
- Vor Änderungen relevante Dateien lesen.
|
||||
- Änderungen möglichst klein und gezielt halten.
|
||||
- Bestehende UI-/Template-Struktur respektieren.
|
||||
- Nach funktionalen Änderungen Tests ergänzen oder anpassen.
|
||||
- Wenn möglich mindestens `.venv/bin/python manage.py test core` ausführen.
|
||||
- Keine grossen Refactorings ohne explizite Nachfrage.
|
||||
- Keine Änderungen an Datenbank/Migrationen ohne Notwendigkeit.
|
||||
- Bei Änderungen an Team-/Tag-Logik besonders auf Seiteneffekte bei Collections, API und Bearbeiten-Formularen achten.
|
||||
|
||||
## Bekannte/Für die Zukunft relevante Themen
|
||||
- Team-Tags und Content-Tags klar getrennt halten.
|
||||
- Bearbeiten von Team-Items darf die Team-Verknüpfung nicht verlieren.
|
||||
- Collections müssen Sichtbarkeit/Team-Kompatibilität von enthaltenen Items beachten.
|
||||
- Mobile UI nutzt eigene Tag-Picker und Quick-Composer-Interaktionen.
|
||||
|
||||
## Notizen für neue Sessions
|
||||
Wenn eine neue Pi-Session in diesem Verzeichnis gestartet wird, zuerst diese Datei, `README.md` und bei Bedarf `core/models.py`, `core/views.py`, `core/forms.py` sowie `core/tests.py` berücksichtigen.
|
||||
@@ -19,6 +19,7 @@
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4"><select class="form-select" name="visibility">
|
||||
<option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option>
|
||||
{% if item.visibility == 'team' %}<option value="team" selected>{% trans "Team" %}{% if item.team %}: {{ item.team.name }}{% endif %}</option>{% endif %}
|
||||
<option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option>
|
||||
</select></div>
|
||||
{% if item.kind == 'todo' %}<div class="col-md-3"><input class="form-control" type="date" name="due_date" value="{{ item.due_at|date:'Y-m-d' }}" title="{% trans 'Due date' %}"></div><div class="col-md-3"><input class="form-control" name="due_time" list="due-times-{{ item.id }}" value="{{ item.due_at|date:'H:i' }}" placeholder="{% trans 'Time optional' %}" title="{% trans 'Time optional' %}"><datalist id="due-times-{{ item.id }}"><option value="00:00"><option value="00:30"><option value="01:00"><option value="01:30"><option value="02:00"><option value="02:30"><option value="03:00"><option value="03:30"><option value="04:00"><option value="04:30"><option value="05:00"><option value="05:30"><option value="06:00"><option value="06:30"><option value="07:00"><option value="07:30"><option value="08:00"><option value="08:30"><option value="09:00"><option value="09:30"><option value="10:00"><option value="10:30"><option value="11:00"><option value="11:30"><option value="12:00"><option value="12:30"><option value="13:00"><option value="13:30"><option value="14:00"><option value="14:30"><option value="15:00"><option value="15:30"><option value="16:00"><option value="16:30"><option value="17:00"><option value="17:30"><option value="18:00"><option value="18:30"><option value="19:00"><option value="19:30"><option value="20:00"><option value="20:30"><option value="21:00"><option value="21:30"><option value="22:00"><option value="22:30"><option value="23:00"><option value="23:30"></datalist></div>{% endif %}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
{% include 'core/_format_toolbar.html' %}
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option><option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option></select></div>
|
||||
<div class="col-md-4"><select class="form-select" name="visibility"><option value="private" {% if item.visibility == 'private' %}selected{% endif %}>{% trans "Private" %}</option>{% if item.visibility == 'team' %}<option value="team" selected>{% trans "Team" %}{% if item.team %}: {{ item.team.name }}{% endif %}</option>{% endif %}<option value="public" {% if item.visibility == 'public' %}selected{% endif %}>{% trans "Public" %}</option></select></div>
|
||||
{% if item.kind == 'todo' %}<div class="col-md-3"><input class="form-control" type="date" name="due_date" value="{{ item.due_at|date:'Y-m-d' }}" title="{% trans 'Due date' %}"></div><div class="col-md-3"><input class="form-control" name="due_time" list="due-times-{{ item.id }}" value="{{ item.due_at|date:'H:i' }}" placeholder="{% trans 'Time optional' %}" title="{% trans 'Time optional' %}"><datalist id="due-times-{{ item.id }}"><option value="00:00"><option value="00:30"><option value="01:00"><option value="01:30"><option value="02:00"><option value="02:30"><option value="03:00"><option value="03:30"><option value="04:00"><option value="04:30"><option value="05:00"><option value="05:30"><option value="06:00"><option value="06:30"><option value="07:00"><option value="07:30"><option value="08:00"><option value="08:30"><option value="09:00"><option value="09:30"><option value="10:00"><option value="10:30"><option value="11:00"><option value="11:30"><option value="12:00"><option value="12:30"><option value="13:00"><option value="13:30"><option value="14:00"><option value="14:30"><option value="15:00"><option value="15:30"><option value="16:00"><option value="16:30"><option value="17:00"><option value="17:30"><option value="18:00"><option value="18:30"><option value="19:00"><option value="19:30"><option value="20:00"><option value="20:30"><option value="21:00"><option value="21:30"><option value="22:00"><option value="22:30"><option value="23:00"><option value="23:30"></datalist></div>{% endif %}
|
||||
<div class="col-md-4"><input class="form-control" type="file" name="files" multiple x-ref="fileInput" @change="previewFile($event)"></div>
|
||||
</div>
|
||||
|
||||
@@ -147,6 +147,39 @@ class NavigationAndLayoutTests(TestCase):
|
||||
self.assertLess(toolbar_position, file_position)
|
||||
|
||||
|
||||
class TeamItemEditTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user(username='team-editor', password='secret')
|
||||
self.team = Team.objects.create(name='Docs team', slug='docs', owner=self.user)
|
||||
TeamMembership.objects.create(team=self.team, user=self.user, role=TeamMembership.Role.OWNER)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_editing_team_item_preserves_team_link(self):
|
||||
item = Item.objects.create(
|
||||
owner=self.user, team=self.team, kind=Item.Kind.TODO,
|
||||
content='Original #docs', visibility=Item.Visibility.TEAM,
|
||||
)
|
||||
item.sync_metadata()
|
||||
|
||||
response = self.client.post(reverse('edit_item', args=[item.id]), {
|
||||
'content': 'Updated #docs', 'comment': '', 'visibility': Item.Visibility.TEAM, 'url': '',
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.team, self.team)
|
||||
self.assertEqual(item.visibility, Item.Visibility.TEAM)
|
||||
|
||||
def test_team_visibility_is_selected_in_edit_forms(self):
|
||||
item = Item.objects.create(
|
||||
owner=self.user, team=self.team, content='Original #docs', visibility=Item.Visibility.TEAM,
|
||||
)
|
||||
response = self.client.get(reverse('edit_item', args=[item.id]))
|
||||
self.assertContains(response, '<option value="team" selected>', html=False)
|
||||
response = self.client.get(reverse('item_editor', args=[item.id]))
|
||||
self.assertContains(response, '<option value="team" selected>', html=False)
|
||||
|
||||
|
||||
class CollectionTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user(username='collector', password='secret')
|
||||
|
||||
Reference in New Issue
Block a user